From e77bcbf6f3bccfc3271269a4ad1fbff00038cfaf Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 23 Sep 2024 20:48:55 +0300 Subject: [PATCH 01/17] fix(dns_adlist): Change an invalid resource name. I don't see any point in keeping the wrong name, since the resource was added recently. Closes #554 --- .../{dns_adlist.md => ip_dns_adlist.md} | 18 +++++++++++++++--- .../routeros_ip_dns_adlist/resource.tf | 2 +- routeros/provider.go | 2 +- routeros/resource_ip_dns_adlist_test.go | 6 +++--- 4 files changed, 20 insertions(+), 8 deletions(-) rename docs/resources/{dns_adlist.md => ip_dns_adlist.md} (50%) diff --git a/docs/resources/dns_adlist.md b/docs/resources/ip_dns_adlist.md similarity index 50% rename from docs/resources/dns_adlist.md rename to docs/resources/ip_dns_adlist.md index 31d2ddda..98261f5b 100644 --- a/docs/resources/dns_adlist.md +++ b/docs/resources/ip_dns_adlist.md @@ -1,7 +1,13 @@ -# routeros_dns_adlist (Resource) - +# routeros_ip_dns_adlist (Resource) +## Example Usage +```terraform +resource "routeros_ip_dns_adlist" "test" { + url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" + ssl_verify = false +} +``` ## Schema @@ -17,4 +23,10 @@ - `id` (String) The ID of this resource. - +## Import +Import is supported using the following syntax: +```shell +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/dns/adlist get [print show-ids]] +terraform import routeros_ip_dns_adlist.test "*0" +``` diff --git a/examples/resources/routeros_ip_dns_adlist/resource.tf b/examples/resources/routeros_ip_dns_adlist/resource.tf index 3ac502f3..17eed2dd 100644 --- a/examples/resources/routeros_ip_dns_adlist/resource.tf +++ b/examples/resources/routeros_ip_dns_adlist/resource.tf @@ -1,4 +1,4 @@ -resource "routeros_dns_adlist" "test" { +resource "routeros_ip_dns_adlist" "test" { url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" ssl_verify = false } diff --git a/routeros/provider.go b/routeros/provider.go index d4a75281..5d8bd823 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -99,6 +99,7 @@ func Provider() *schema.Provider { "routeros_ip_pool": ResourceIPPool(), "routeros_ip_route": ResourceIPRoute(), "routeros_ip_dns": ResourceDns(), + "routeros_ip_dns_adlist": ResourceDnsAdlist(), "routeros_ip_dns_record": ResourceDnsRecord(), "routeros_ip_service": ResourceIpService(), "routeros_ip_neighbor_discovery_settings": ResourceIpNeighborDiscoverySettings(), @@ -125,7 +126,6 @@ func Provider() *schema.Provider { "routeros_firewall_mangle": ResourceIPFirewallMangle(), "routeros_firewall_nat": ResourceIPFirewallNat(), "routeros_dns": ResourceDns(), - "routeros_dns_adlist": ResourceDnsAdlist(), "routeros_dns_record": ResourceDnsRecord(), // Interface Objects diff --git a/routeros/resource_ip_dns_adlist_test.go b/routeros/resource_ip_dns_adlist_test.go index eeec19ba..8dcb91ac 100644 --- a/routeros/resource_ip_dns_adlist_test.go +++ b/routeros/resource_ip_dns_adlist_test.go @@ -7,7 +7,7 @@ import ( ) const testDnsAdlistMinVersion = "7.15" -const testResourceDnsAdlist = "routeros_dns_adlist.test" +const testResourceDnsAdlist = "routeros_ip_dns_adlist.test" func TestAccResourceDnsAdlistTest_basic(t *testing.T) { if !testCheckMinVersion(t, testDnsAdlistMinVersion) { @@ -23,7 +23,7 @@ func TestAccResourceDnsAdlistTest_basic(t *testing.T) { testSetTransportEnv(t, name) }, ProviderFactories: testAccProviderFactories, - CheckDestroy: testCheckResourceDestroy("/ip/dns/adlist", "routeros_dns_adlist"), + CheckDestroy: testCheckResourceDestroy("/ip/dns/adlist", "routeros_ip_dns_adlist"), Steps: []resource.TestStep{ { Config: testAccResourceDnsAdlistConfig(), @@ -42,7 +42,7 @@ func TestAccResourceDnsAdlistTest_basic(t *testing.T) { func testAccResourceDnsAdlistConfig() string { return providerConfig + ` -resource "routeros_dns_adlist" "test" { +resource "routeros_ip_dns_adlist" "test" { url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" ssl_verify = false }` From f42cce719ef5078db0048002013718bc0dd9eb57 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 10:46:48 +0300 Subject: [PATCH 02/17] chore: Change the generation of resources. Added possibility to create a raw resource suitable for further editing from a template and CSV table with attributes description. --- .gitignore | 5 ++- tools/boilerplate/main.go | 70 ++++++++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 0c365ac5..713ecdff 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ pkg/* issues/* terraform-provider-routeros* terraform.tfstate* -node_modules \ No newline at end of file +node_modules +tools/boilerplate/examples/** +tools/boilerplate/routeros/** +tools/boilerplate/*.csv \ No newline at end of file diff --git a/tools/boilerplate/main.go b/tools/boilerplate/main.go index 0fd0575d..bc4be022 100644 --- a/tools/boilerplate/main.go +++ b/tools/boilerplate/main.go @@ -3,6 +3,7 @@ package main import ( "bufio" + "bytes" "flag" "fmt" "log" @@ -21,7 +22,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") + csvTable = flag.String("table", "", "Extracting attributes from the WIKI table (CSV file)") ) func Fatalf(format string, a ...any) { @@ -63,15 +64,7 @@ func main() { flag.Parse() if len(flag.Args()) < 1 { - 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) + Fatalf("Usage: go run tools/bolerplate/main.go [-table file.csv] [-system] ") } resName := flag.Args()[0] @@ -79,6 +72,14 @@ func main() { Fatalf("The resource name must be in the format: 'routeros_[a-z_]+', got '%v'", resName) } + var Schema string + if *csvTable != "" { + if _, err := os.Stat(*csvTable); err != nil { + Fatalf("CSV file %v not found", *csvTable) + } + Schema = extractAttributes(*csvTable) + } + itemType := Resource // if *isDS { // itemType = Datasource @@ -107,7 +108,8 @@ func main() { err = tmpl.Execute(f, struct { GoResourceName string System bool - }{Resource.String() + goName, *isSystem}) + Schema string + }{Resource.String() + goName, *isSystem, Schema}) if err != nil { panic(err) } @@ -257,6 +259,7 @@ func {{.GoResourceName}}() *schema.Resource { MetaResourcePath: PropResourcePath("/"), MetaId: PropId(Id), + {{.Schema}} } return &schema.Resource{ @@ -313,7 +316,36 @@ var ( enumReplacer = strings.NewReplacer(" ", "", `"`, "`", "'", "`", "|", `", "`) ) -func extractAttributes(filename string) { +func splitDescription(s string) (res string) { + if len(s) == 0 { + return + } + + s = string(unicode.ToUpper(rune(s[0]))) + s[1:] + if s[len(s)-1] != '.' { + s += "." + } + + if len(s) < 86 { + return s + } + + var maxLen = 90 + var i int + for _, c := range s { + res += string(c) + i++ + + if c == ' ' && i >= maxLen { + res += "\" +\n \"" + maxLen = 100 + i = 0 + } + } + return +} + +func extractAttributes(filename string) string { tmpl, err := template.New("attr").Parse(attribute) if err != nil { panic(err) @@ -326,14 +358,14 @@ func extractAttributes(filename string) { } defer file.Close() - w := os.Stdout + ww := bytes.NewBuffer(nil) scanner := bufio.NewScanner(file) for scanner.Scan() { row := scanner.Text() rec := reCSV.FindAllStringSubmatch(row, -1) if len(rec) != 2 { - fmt.Fprintln(w, row) + fmt.Fprintln(ww, row) continue } @@ -354,7 +386,7 @@ func extractAttributes(filename string) { } // [ ["Property", Property] ["Description" Description] ] - if r1 == "Property" && r2 == "Description" { + if (r1 == "Property" || r1 == "Parameters") && r2 == "Description" { continue } @@ -382,8 +414,6 @@ func extractAttributes(filename string) { validate = enumReplacer.Replace(match[1]) } - ww := os.Stdout - tmpl.Execute(ww, struct { Attribute string Type string @@ -393,17 +423,19 @@ func extractAttributes(filename string) { }{ Attribute: strings.ReplaceAll(reAttrName.FindString(r1), "-", "_"), Type: attrType, - Description: strings.ReplaceAll(r2, `"`, "`"), + Description: splitDescription(strings.ReplaceAll(r2, `"`, "`")), Slice: validate, DiffSuppress: diffSuppress, }) if r1 == "type" { - os.Exit(0) + return ww.String() } if err != nil { Fatalf("%v", err) } } + + return ww.String() } From f0469663fe7ea395825dc92c6a246e09dcd0f81c Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 10:50:35 +0300 Subject: [PATCH 03/17] feat: Add new resource `routeros_tool_sniffer` Possible problems with running on old ROS, because attribute names have changed. --- .../resources/routeros_tool_sniffer/import.sh | 1 + .../routeros_tool_sniffer/resource.tf | 9 + routeros/provider.go | 1 + routeros/resource_tool_sniffer.go | 403 ++++++++++++++++++ routeros/resource_tool_sniffer_test.go | 67 +++ 5 files changed, 481 insertions(+) create mode 100755 examples/resources/routeros_tool_sniffer/import.sh create mode 100755 examples/resources/routeros_tool_sniffer/resource.tf create mode 100755 routeros/resource_tool_sniffer.go create mode 100755 routeros/resource_tool_sniffer_test.go diff --git a/examples/resources/routeros_tool_sniffer/import.sh b/examples/resources/routeros_tool_sniffer/import.sh new file mode 100755 index 00000000..6aaaf86a --- /dev/null +++ b/examples/resources/routeros_tool_sniffer/import.sh @@ -0,0 +1 @@ +terraform import routeros_tool_sniffer.test . \ No newline at end of file diff --git a/examples/resources/routeros_tool_sniffer/resource.tf b/examples/resources/routeros_tool_sniffer/resource.tf new file mode 100755 index 00000000..ff204865 --- /dev/null +++ b/examples/resources/routeros_tool_sniffer/resource.tf @@ -0,0 +1,9 @@ +resource "routeros_tool_sniffer" "test" { + streaming_enabled = true + streaming_server = "192.168.88.5:37008" + filter_stream = true + + filter_interface = ["ether2"] + filter_direction = "rx" + filter_operator_between_entries = "and" +} \ No newline at end of file diff --git a/routeros/provider.go b/routeros/provider.go index 5d8bd823..eb31731c 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -262,6 +262,7 @@ func Provider() *schema.Provider { "routeros_tool_mac_server": ResourceToolMacServer(), "routeros_tool_mac_server_winbox": ResourceToolMacServerWinBox(), "routeros_tool_netwatch": ResourceToolNetwatch(), + "routeros_tool_sniffer": ResourceToolSniffer(), // User Manager "routeros_user_manager_advanced": ResourceUserManagerAdvanced(), diff --git a/routeros/resource_tool_sniffer.go b/routeros/resource_tool_sniffer.go new file mode 100755 index 00000000..192ba5df --- /dev/null +++ b/routeros/resource_tool_sniffer.go @@ -0,0 +1,403 @@ +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" +) + +/* +{ + "file-limit": "1000", + "file-name": "", + "filter-cpu": "", + "filter-direction": "any", + "filter-dst-ip-address": "", + "filter-dst-ipv6-address": "", + "filter-dst-mac-address": "", + "filter-dst-port": "", + "filter-interface": "", + "filter-ip-address": "", + "filter-ip-protocol": "", + "filter-ipv6-address": "", + "filter-mac-address": "", + "filter-mac-protocol": "", + "filter-operator-between-entries": "or", + "filter-port": "", + "filter-size": "", + "filter-src-ip-address": "", + "filter-src-ipv6-address": "", + "filter-src-mac-address": "", + "filter-src-port": "", + "filter-stream": "false", + "filter-vlan": "", + "memory-limit": "100", + "memory-scroll": "true", + "only-headers": "false", + "quick-rows": "20", + "quick-show-frame": "false", + "running": "false", + "streaming-enabled": "false", + "streaming-server": "0.0.0.0:37008" +} +*/ + +// https://help.mikrotik.com/docs/display/ROS/Packet+Sniffer +func ResourceToolSniffer() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/tool/sniffer"), + MetaId: PropId(Id), + MetaSkipFields: PropSkipFields("quick_rows", "quick_show_frame", "show_frame"), + + "file_limit": { + Type: schema.TypeInt, + Optional: true, + Description: "File size limit. Sniffer will stop when a limit is reached.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "file_name": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the file where sniffed packets will be saved.", + }, + "filter_cpu": { + Type: schema.TypeString, + Optional: true, + Description: "CPU core used as a filter.", + }, + "filter_direction": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies which direction filtering will be applied.", + ValidateFunc: validation.StringInSlice([]string{"any", "rx", "tx"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "filter_dst_mac_address": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 MAC destination addresses and MAC address masks used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsMACAddress, + }, + MaxItems: 16, + }, + "filter_dst_ip_address": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 IP destination addresses used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv4Address, + }, + MaxItems: 16, + }, + "filter_dst_ipv6_address": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 IPv6 destination addresses used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv6Address, + }, + MaxItems: 16, + }, + "filter_dst_port": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 comma-separated destination ports used as a filter. A list of predefined port names " + + "is also available, like ssh and telnet.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + MaxItems: 16, + }, + "filter_interface": { + Type: schema.TypeSet, + Optional: true, + Description: "Interface name on which sniffer will be running. all indicates that the sniffer will sniff " + + "packets on all interfaces.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + MaxItems: 16, + }, + "filter_ip_address": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 IP addresses used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv4Address, + }, + MaxItems: 16, + }, + "filter_ipv6_address": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 IPv6 addresses used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv6Address, + }, + MaxItems: 16, + }, + "filter_ip_protocol": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 comma-separated IP/IPv6 protocols used as a filter. IP protocols (instead of protocol " + + "names, protocol numbers can be used):\n" + + "* ipsec-ah - IPsec AH protocol\n" + + "* ipsec-esp - IPsec ESP protocol\n" + + "* ddp - datagram delivery protocol\n" + + "* egp - exterior gateway protocol\n" + + "* ggp - gateway-gateway protocol\n" + + "* gre - general routing encapsulation\n" + + "* hmp - host monitoring protocol\n" + + "* idpr-cmtp - idpr control message transport\n" + + "* icmp - internet control message protocol\n" + + "* icmpv6 - internet control message protocol v6\n" + + "* igmp - internet group management protocol\n" + + "* ipencap - ip encapsulated in ip\n" + + "* ipip - ip encapsulation\n" + + "* encap - ip encapsulation\n" + + "* iso-tp4 - iso transport protocol class 4\n" + + "* ospf - open shortest path first\n" + + "* pup - parc universal packet protocol\n" + + "* pim - protocol independent multicast\n" + + "* rspf - radio shortest path first\n" + + "* rdp - reliable datagram protocol\n" + + "* st - st datagram mode\n" + + "* tcp - transmission control protocol\n" + + "* udp - user datagram protocol\n" + + "* vmtp versatile message transport\n" + + "* vrrp - virtual router redundancy protocol\n" + + "* xns-idp - xerox xns idp\n" + + "* xtp - xpress transfer protocol", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: ValidationValInSlice([]string{}, false, true), + }, + MaxItems: 16, + }, + "filter_mac_address": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 MAC addresses and MAC address masks used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsMACAddress, + }, + MaxItems: 16, + }, + "filter_mac_protocol": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 comma separated entries used as a filter. Mac protocols (instead of protocol names, " + + "protocol number can be used):\n" + + "* 802.2 - 802.2 Frames (0x0004)\n" + + "* arp - Address Resolution Protocol (0x0806)\n" + + "* homeplug-av - HomePlug AV MME (0x88E1)\n" + + "* ip - Internet Protocol version 4 (0x0800)\n" + + "* ipv6 - Internet Protocol Version 6 (0x86DD)\n" + + "* ipx - Internetwork Packet Exchange (0x8137)\n" + + "* lldp - Link Layer Discovery Protocol (0x88CC)\n" + + "* loop-protect - Loop Protect Protocol (0x9003)\n" + + "* mpls-multicast - MPLS multicast (0x8848)\n" + + "* mpls-unicast - MPLS unicast (0x8847)\n" + + "* packing-compr - Encapsulated packets with compressed IP packing (0x9001)\n" + + "* packing-simple - Encapsulated packets with simple IP packing (0x9000)\n" + + "* pppoe - PPPoE Session Stage (0x8864)\n" + + "* pppoe-discovery - PPPoE Discovery Stage (0x8863)\n" + + "* rarp - Reverse Address Resolution Protocol (0x8035)\n" + + "* service-vlan - Provider Bridging (IEEE 802.1ad) & Shortest Path Bridging IEEE 802.1aq (0x88A8)\n" + + "* vlan - VLAN-tagged frame (IEEE 802.1Q) and Shortest Path Bridging IEEE 802.1aq with NNI compatibility (0x8100)", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + MaxItems: 16, + }, + "filter_operator_between_entries": { + Type: schema.TypeString, + Optional: true, + Description: "Changes the logic for filters with multiple entries.", + ValidateFunc: validation.StringInSlice([]string{"and", "or"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "filter_port": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 comma-separated ports used as a filter. A list of predefined port names is also available, " + + "like ssh and telnet.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + MaxItems: 16, + }, + // Type attribute string instead of number because MT returns an empty string value. + "filter_size": { + Type: schema.TypeString, + Optional: true, + Description: "Filters packets of specified size or size range in bytes.", + }, + "filter_src_mac_address": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 MAC source addresses and MAC address masks used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsMACAddress, + }, + MaxItems: 16, + }, + "filter_src_ip_address": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 IP source addresses used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv4Address, + }, + MaxItems: 16, + }, + "filter_src_ipv6_address": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 IPv6 source addresses used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv6Address, + }, + MaxItems: 16, + }, + "filter_src_port": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 comma-separated source ports used as a filter. A list of predefined port names is " + + "also available, like ssh and telnet.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + MaxItems: 16, + }, + "filter_stream": { + Type: schema.TypeBool, + Optional: true, + Description: "Sniffed packets that are devised for the sniffer server are ignored.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "filter_vlan": { + Type: schema.TypeSet, + Optional: true, + Description: "Up to 16 VLAN IDs used as a filter.", + Elem: &schema.Schema{ + Type: schema.TypeInt, + ValidateFunc: validation.IntBetween(0, 4095), + }, + }, + "memory_limit": { + Type: schema.TypeInt, + Optional: true, + Description: "Memory amount used to store sniffed data.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "memory_scroll": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to rewrite older sniffed data when the memory limit is reached.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "only_headers": { + Type: schema.TypeBool, + Optional: true, + Description: "Save in the memory only the packet's headers, not the whole packet.", + }, + KeyRunning: PropRunningRo, + "streaming_enabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Defines whether to send sniffed packets to the streaming server.", + }, + "streaming_server": { + Type: schema.TypeString, + Optional: true, + Description: "Tazmen Sniffer Protocol (TZSP) stream receiver.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + } + + return &schema.Resource{ + CreateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + diags := SystemResourceCreateUpdate(ctx, resSchema, d, m) + if diags.HasError() { + return diags + } + + startSniffer(ctx, resSchema, d, m) + + return SystemResourceRead(ctx, resSchema, d, m) + }, + + ReadContext: DefaultSystemRead(resSchema), + + UpdateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + stopSniffer(ctx, resSchema, d, m) + + diags := SystemResourceCreateUpdate(ctx, resSchema, d, m) + if diags.HasError() { + return diags + } + startSniffer(ctx, resSchema, d, m) + + return SystemResourceRead(ctx, resSchema, d, m) + }, + + DeleteContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + stopSniffer(ctx, resSchema, d, m) + + return SystemResourceDelete(ctx, resSchema, d, m) + }, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} + +func startSniffer(ctx context.Context, s map[string]*schema.Schema, d *schema.ResourceData, m interface{}) diag.Diagnostics { + // Start sniffer. + var resUrl = &URL{ + Path: s[MetaResourcePath].Default.(string), + } + if m.(Client).GetTransport() == TransportREST { + resUrl.Path += "/start" + } + + err := m.(Client).SendRequest(crudStart, resUrl, MikrotikItem{}, nil) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func stopSniffer(ctx context.Context, s map[string]*schema.Schema, d *schema.ResourceData, m interface{}) diag.Diagnostics { + // Stop sniffer. + var resUrl = &URL{ + Path: s[MetaResourcePath].Default.(string), + } + if m.(Client).GetTransport() == TransportREST { + resUrl.Path += "/stop" + } + + err := m.(Client).SendRequest(crudStop, resUrl, MikrotikItem{}, nil) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/routeros/resource_tool_sniffer_test.go b/routeros/resource_tool_sniffer_test.go new file mode 100755 index 00000000..407751a0 --- /dev/null +++ b/routeros/resource_tool_sniffer_test.go @@ -0,0 +1,67 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testToolSniffer = "routeros_tool_sniffer.test" + +func TestAccToolSnifferTest_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: testAccToolSnifferConfig("or"), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testToolSniffer), + resource.TestCheckResourceAttr(testToolSniffer, "streaming_enabled", "true"), + resource.TestCheckResourceAttr(testToolSniffer, "streaming_server", "192.168.88.5:37008"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_stream", "true"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_interface.0", "ether2"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_direction", "rx"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_operator_between_entries", "or"), + ), + }, + { + Config: testAccToolSnifferConfig("and"), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testToolSniffer), + resource.TestCheckResourceAttr(testToolSniffer, "streaming_enabled", "true"), + resource.TestCheckResourceAttr(testToolSniffer, "streaming_server", "192.168.88.5:37008"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_stream", "true"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_interface.0", "ether2"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_direction", "rx"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_operator_between_entries", "and"), + ), + }, + }, + }) + + }) + } +} + +func testAccToolSnifferConfig(param string) string { + return fmt.Sprintf(`%v + +resource "routeros_tool_sniffer" "test" { + streaming_enabled = true + streaming_server = "192.168.88.5:37008" + filter_stream = true + + filter_interface = ["ether2"] + filter_direction = "rx" + filter_operator_between_entries = "%v" +} +`, providerConfig, param) +} From 2a18b1e126fe635335b336a736629ed8b6546423 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 13:02:03 +0300 Subject: [PATCH 04/17] chore(boilerplate): Add ResourcePath --- tools/boilerplate/main.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/boilerplate/main.go b/tools/boilerplate/main.go index bc4be022..a8225dee 100644 --- a/tools/boilerplate/main.go +++ b/tools/boilerplate/main.go @@ -127,7 +127,8 @@ func main() { err = tmpl.Execute(f, struct { GoResourceName string ResourceName string - }{goName, resName}) + ResourcePath string + }{goName, resName, strings.ReplaceAll(strings.TrimPrefix(resName, "routeros_"), "_", "/")}) if err != nil { panic(err) } @@ -145,7 +146,8 @@ func main() { } err = tmpl.Execute(f, struct { ResourceName string - }{resName}) + ResourcePath string + }{resName, strings.ReplaceAll(strings.TrimPrefix(resName, "routeros_"), "_", "/")}) if err != nil { panic(err) } @@ -182,7 +184,7 @@ func main() { } var exampleImportFile = `#The ID can be found via API or the terminal -#The command for the terminal is -> :put [/ get [print show-ids]] +#The command for the terminal is -> :put [/{{.ResourcePath}} get [print show-ids]] terraform import {{.ResourceName}}.test *3` var exampleResourceFile = ` @@ -211,6 +213,7 @@ func TestAcc{{.GoResourceName}}Test_basic(t *testing.T) { testSetTransportEnv(t, name) }, ProviderFactories: testAccProviderFactories, + CheckDestroy: testCheckResourceDestroy("/{{.ResourcePath}}", "{{.ResourceName}}"), Steps: []resource.TestStep{ { Config: testAcc{{.GoResourceName}}Config(""), From 9c111eeefa0ce90e0bc2f02c9c2d9a4794487cbe Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 13:03:50 +0300 Subject: [PATCH 05/17] feat(hotspot): Add new resource `routeros_ip_hotspot_walled_garden` --- .../import.sh | 3 + .../resource.tf | 5 ++ routeros/provider.go | 11 +-- routeros/resource_ip_hotspot_walled_garden.go | 86 +++++++++++++++++++ .../resource_ip_hotspot_walled_garden_test.go | 49 +++++++++++ 5 files changed, 149 insertions(+), 5 deletions(-) create mode 100755 examples/resources/routeros_ip_hotspot_walled_garden/import.sh create mode 100755 examples/resources/routeros_ip_hotspot_walled_garden/resource.tf create mode 100644 routeros/resource_ip_hotspot_walled_garden.go create mode 100644 routeros/resource_ip_hotspot_walled_garden_test.go diff --git a/examples/resources/routeros_ip_hotspot_walled_garden/import.sh b/examples/resources/routeros_ip_hotspot_walled_garden/import.sh new file mode 100755 index 00000000..dd8678ff --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_walled_garden/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/hotspot/walled-garden get [print show-ids]] +terraform import routeros_ip_hotspot_walled_garden.test *3 \ No newline at end of file diff --git a/examples/resources/routeros_ip_hotspot_walled_garden/resource.tf b/examples/resources/routeros_ip_hotspot_walled_garden/resource.tf new file mode 100755 index 00000000..fab64ae8 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_walled_garden/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_ip_hotspot_walled_garden" "test" { + action = "deny" + dst_host = "1.2.3.4" + dst_port = "!443" +} \ No newline at end of file diff --git a/routeros/provider.go b/routeros/provider.go index eb31731c..44ba190c 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -80,6 +80,7 @@ func Provider() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ // IP objects + "routeros_ip_address": ResourceIPAddress(), "routeros_ip_dhcp_client": ResourceDhcpClient(), "routeros_ip_dhcp_client_option": ResourceDhcpClientOption(), "routeros_ip_dhcp_relay": ResourceDhcpRelay(), @@ -89,20 +90,20 @@ func Provider() *schema.Provider { "routeros_ip_dhcp_server_lease": ResourceDhcpServerLease(), "routeros_ip_dhcp_server_option": ResourceDhcpServerOption(), "routeros_ip_dhcp_server_option_set": ResourceDhcpServerOptionSet(), + "routeros_ip_dns": ResourceDns(), + "routeros_ip_dns_adlist": ResourceDnsAdlist(), + "routeros_ip_dns_record": ResourceDnsRecord(), "routeros_ip_firewall_addr_list": ResourceIPFirewallAddrList(), "routeros_ip_firewall_connection_tracking": ResourceIPConnectionTracking(), "routeros_ip_firewall_filter": ResourceIPFirewallFilter(), "routeros_ip_firewall_mangle": ResourceIPFirewallMangle(), "routeros_ip_firewall_nat": ResourceIPFirewallNat(), "routeros_ip_firewall_raw": ResourceIPFirewallRaw(), - "routeros_ip_address": ResourceIPAddress(), + "routeros_ip_hotspot_walled_garden": ResourceIpHotspotWalledGarden(), + "routeros_ip_neighbor_discovery_settings": ResourceIpNeighborDiscoverySettings(), "routeros_ip_pool": ResourceIPPool(), "routeros_ip_route": ResourceIPRoute(), - "routeros_ip_dns": ResourceDns(), - "routeros_ip_dns_adlist": ResourceDnsAdlist(), - "routeros_ip_dns_record": ResourceDnsRecord(), "routeros_ip_service": ResourceIpService(), - "routeros_ip_neighbor_discovery_settings": ResourceIpNeighborDiscoverySettings(), "routeros_ip_ssh_server": ResourceIpSSHServer(), "routeros_ip_upnp": ResourceUPNPSettings(), "routeros_ip_upnp_interfaces": ResourceUPNPInterfaces(), diff --git a/routeros/resource_ip_hotspot_walled_garden.go b/routeros/resource_ip_hotspot_walled_garden.go new file mode 100644 index 00000000..4aaad0b9 --- /dev/null +++ b/routeros/resource_ip_hotspot_walled_garden.go @@ -0,0 +1,86 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* +{ + ".id": "*6", + "action": "deny", + "disabled": "false", + "dst-host": "1.2.3.4", + "dst-port": "!123", + "dynamic": "false", + "hits": "0", + "method": "GET", + "path": "/sss", + "server": "server1", + "src-address": "4.3.2.1" +} +*/ + +// https://wiki.mikrotik.com/wiki/Manual:IP/Hotspot/Walled_Garden +func ResourceIpHotspotWalledGarden() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/hotspot/walled-garden"), + MetaId: PropId(Id), + MetaSkipFields: PropSkipFields("hits", "dst_address"), + + "action": { + Type: schema.TypeString, + Optional: true, + Description: "Action to perform, when packet matches the rule `allow` - allow access to the web-page without " + + "authorization, `deny` - the authorization is required to access the web-page.", + ValidateFunc: validation.StringInSlice([]string{"allow", "deny"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "dst_host": { + Type: schema.TypeString, + Optional: true, + Description: "Domain name of the destination web-server.", + }, + "dst_port": { + Type: schema.TypeString, + Optional: true, + Description: "TCP port number, client sends request to.", + }, + KeyDynamic: PropDynamicRo, + "method": { + Type: schema.TypeString, + Optional: true, + Description: "HTTP method of the request.", + }, + "path": { + Type: schema.TypeString, + Optional: true, + Description: "The path of the request, path comes after `http://dst_host/`.", + }, + "server": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the HotSpot server, rule is applied to.", + }, + "src_address": { + Type: schema.TypeString, + Optional: true, + Description: "Source address of the user, usually IP address of the HotSpot client.", + }, + } + + 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_ip_hotspot_walled_garden_test.go b/routeros/resource_ip_hotspot_walled_garden_test.go new file mode 100644 index 00000000..fb9044df --- /dev/null +++ b/routeros/resource_ip_hotspot_walled_garden_test.go @@ -0,0 +1,49 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpHotspotWalledGarden = "routeros_ip_hotspot_walled_garden.test" + +func TestAccIpHotspotWalledGardenTest_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, + CheckDestroy: testCheckResourceDestroy("/ip/hotspot/walled-garden", "routeros_ip_hotspot_walled_garden"), + Steps: []resource.TestStep{ + { + Config: testAccIpHotspotWalledGardenConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpHotspotWalledGarden), + resource.TestCheckResourceAttr(testIpHotspotWalledGarden, "action", "deny"), + resource.TestCheckResourceAttr(testIpHotspotWalledGarden, "dst_host", "1.2.3.4"), + resource.TestCheckResourceAttr(testIpHotspotWalledGarden, "dst_port", "!443"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpHotspotWalledGardenConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_hotspot_walled_garden" "test" { + action = "deny" + dst_host = "1.2.3.4" + dst_port = "!443" +} +`, providerConfig) +} From d86a9ba39036c8657101edf8551123f5946c7c5f Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 13:09:17 +0300 Subject: [PATCH 06/17] chore(chmod): Fix the file rights --- routeros/resource_tool_sniffer.go | 0 routeros/resource_tool_sniffer_test.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 routeros/resource_tool_sniffer.go mode change 100755 => 100644 routeros/resource_tool_sniffer_test.go diff --git a/routeros/resource_tool_sniffer.go b/routeros/resource_tool_sniffer.go old mode 100755 new mode 100644 diff --git a/routeros/resource_tool_sniffer_test.go b/routeros/resource_tool_sniffer_test.go old mode 100755 new mode 100644 From bfd85d5817825ee2ce06ce671cc50a6081abc544 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 13:25:11 +0300 Subject: [PATCH 07/17] chore(boilerplate): Fix FileMode --- tools/boilerplate/main.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tools/boilerplate/main.go b/tools/boilerplate/main.go index a8225dee..deb6722e 100644 --- a/tools/boilerplate/main.go +++ b/tools/boilerplate/main.go @@ -96,7 +96,7 @@ func main() { // 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) + f, err := os.OpenFile(filepath.Join("routeros", fName+".go"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) if err != nil { panic(err) } @@ -109,13 +109,14 @@ func main() { GoResourceName string System bool Schema string - }{Resource.String() + goName, *isSystem, Schema}) + ResourcePath string + }{Resource.String() + goName, *isSystem, Schema, strings.ReplaceAll(strings.TrimPrefix(resName, "routeros_"), "_", "/")}) if err != nil { panic(err) } f.Close() - f, err = os.OpenFile(filepath.Join("routeros", fName+"_test.go"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) + f, err = os.OpenFile(filepath.Join("routeros", fName+"_test.go"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) if err != nil { panic(err) } @@ -136,7 +137,7 @@ func main() { os.MkdirAll(filepath.Join("examples", "resources", resName), os.ModePerm) - f, err = os.OpenFile(filepath.Join("examples", "resources", resName, "import.sh"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) + f, err = os.OpenFile(filepath.Join("examples", "resources", resName, "import.sh"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) if err != nil { panic(err) } @@ -153,7 +154,7 @@ func main() { } f.Close() - f, err = os.OpenFile(filepath.Join("examples", "resources", resName, "resource.tf"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) + f, err = os.OpenFile(filepath.Join("examples", "resources", resName, "resource.tf"), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) if err != nil { panic(err) } @@ -174,7 +175,7 @@ func main() { flags |= os.O_CREATE } - f, err = os.OpenFile(filepath.Join("routeros", "provider.go"), flags, os.ModePerm) + f, err = os.OpenFile(filepath.Join("routeros", "provider.go"), flags, 0644) if err != nil { panic(err) } @@ -259,7 +260,7 @@ REST JSON // https://help.mikrotik.com/docs/display/ROS/ func {{.GoResourceName}}() *schema.Resource { resSchema := map[string]*schema.Schema{ - MetaResourcePath: PropResourcePath("/"), + MetaResourcePath: PropResourcePath("/{{.ResourcePath}}"), MetaId: PropId(Id), {{.Schema}} From 92778ff184124ce8d2d7dc0dfc266b29b6f306ef Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 13:39:16 +0300 Subject: [PATCH 08/17] feat(hotspot): Add new resource `routeros_ip_hotspot_walled_garden_ip` --- .../import.sh | 3 + .../resource.tf | 9 ++ routeros/provider.go | 1 + .../resource_ip_hotspot_walled_garden_ip.go | 100 ++++++++++++++++++ ...source_ip_hotspot_walled_garden_ip_test.go | 57 ++++++++++ 5 files changed, 170 insertions(+) create mode 100755 examples/resources/routeros_ip_hotspot_walled_garden_ip/import.sh create mode 100755 examples/resources/routeros_ip_hotspot_walled_garden_ip/resource.tf create mode 100644 routeros/resource_ip_hotspot_walled_garden_ip.go create mode 100644 routeros/resource_ip_hotspot_walled_garden_ip_test.go diff --git a/examples/resources/routeros_ip_hotspot_walled_garden_ip/import.sh b/examples/resources/routeros_ip_hotspot_walled_garden_ip/import.sh new file mode 100755 index 00000000..2a1481ff --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_walled_garden_ip/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/hotspot/walled-garden/ip get [print show-ids]] +terraform import routeros_ip_hotspot_walled_garden_ip.test *3 \ No newline at end of file diff --git a/examples/resources/routeros_ip_hotspot_walled_garden_ip/resource.tf b/examples/resources/routeros_ip_hotspot_walled_garden_ip/resource.tf new file mode 100755 index 00000000..f759becf --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_walled_garden_ip/resource.tf @@ -0,0 +1,9 @@ +resource "routeros_ip_hotspot_walled_garden_ip" "test" { + action = "reject" + dst_address = "!0.0.0.0" + dst_address_list = "dlist" + dst_port = "0-65535" + protocol = "tcp" + src_address = "0.0.0.0" + src_address_list = "slist" +} diff --git a/routeros/provider.go b/routeros/provider.go index 44ba190c..de90aa3b 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -100,6 +100,7 @@ func Provider() *schema.Provider { "routeros_ip_firewall_nat": ResourceIPFirewallNat(), "routeros_ip_firewall_raw": ResourceIPFirewallRaw(), "routeros_ip_hotspot_walled_garden": ResourceIpHotspotWalledGarden(), + "routeros_ip_hotspot_walled_garden_ip": ResourceIpHotspotWalledGardenIp(), "routeros_ip_neighbor_discovery_settings": ResourceIpNeighborDiscoverySettings(), "routeros_ip_pool": ResourceIPPool(), "routeros_ip_route": ResourceIPRoute(), diff --git a/routeros/resource_ip_hotspot_walled_garden_ip.go b/routeros/resource_ip_hotspot_walled_garden_ip.go new file mode 100644 index 00000000..e450a4a5 --- /dev/null +++ b/routeros/resource_ip_hotspot_walled_garden_ip.go @@ -0,0 +1,100 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* +{ + ".id": "*4", + "action": "reject", + "disabled": "false", + "dst-address": "!0.0.0.0", + "dst-address-list": "bbb", + "dst-port": "0-65535", + "invalid": "false", + "protocol": "tcp", + "server": "server1", + "src-address": "0.0.0.0", + "src-address-list": "aaa" +} +*/ + +// https://wiki.mikrotik.com/wiki/Manual:IP/Hotspot/Walled_Garden#IP_Walled_Garden +func ResourceIpHotspotWalledGardenIp() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/hotspot/walled-garden/ip"), + MetaId: PropId(Id), + + "action": { + Type: schema.TypeString, + Optional: true, + Description: "Action to perform, when packet matches the rule allow - allow access to the web-page without " + + "authorization deny - the authorization is required to access the web-page reject - the authorization " + + "is required to access the resource, ICMP reject message will be sent to client, when packet will match " + + "the rule.", + ValidateFunc: validation.StringInSlice([]string{"allow", "deny", "reject"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "dst_address": { + Type: schema.TypeString, + Optional: true, + Description: "Destination IP address, IP address of the WEB-server. Ignored if dst-host is already specified.", + ConflictsWith: []string{"dst_host"}, + }, + "dst_address_list": { + Type: schema.TypeString, + Optional: true, + Description: "Destination IP address list. Ignored if dst-host is already specified.", + }, + "dst_host": { + Type: schema.TypeString, + Optional: true, + Description: "Domain name of the destination web-server. When this parameter is specified dynamic entry " + + "is added to Walled Garden.", + ConflictsWith: []string{"dst_address"}, + }, + "dst_port": { + Type: schema.TypeString, + Optional: true, + Description: "TCP port number, client sends request to.", + }, + KeyInvalid: PropInvalidRo, + "protocol": { + Type: schema.TypeString, + Optional: true, + Description: "IP protocol.", + }, + "server": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the HotSpot server, rule is applied to.", + }, + "src_address": { + Type: schema.TypeString, + Optional: true, + Description: "Source address of the user, usually IP address of the HotSpot client.", + }, + "src_address_list": { + Type: schema.TypeString, + Optional: true, + Description: "Source IP address list.", + }, + } + + 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_ip_hotspot_walled_garden_ip_test.go b/routeros/resource_ip_hotspot_walled_garden_ip_test.go new file mode 100644 index 00000000..534d0c92 --- /dev/null +++ b/routeros/resource_ip_hotspot_walled_garden_ip_test.go @@ -0,0 +1,57 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpHotspotWalledGardenIp = "routeros_ip_hotspot_walled_garden_ip.test" + +func TestAccIpHotspotWalledGardenIpTest_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, + CheckDestroy: testCheckResourceDestroy("/ip/hotspot/walled-garden/ip", "routeros_ip_hotspot_walled_garden_ip"), + Steps: []resource.TestStep{ + { + Config: testAccIpHotspotWalledGardenIpConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpHotspotWalledGardenIp), + resource.TestCheckResourceAttr(testIpHotspotWalledGardenIp, "action", "reject"), + resource.TestCheckResourceAttr(testIpHotspotWalledGardenIp, "dst_address", "!0.0.0.0"), + resource.TestCheckResourceAttr(testIpHotspotWalledGardenIp, "dst_address_list", "dlist"), + resource.TestCheckResourceAttr(testIpHotspotWalledGardenIp, "dst_port", "0-65535"), + resource.TestCheckResourceAttr(testIpHotspotWalledGardenIp, "protocol", "tcp"), + resource.TestCheckResourceAttr(testIpHotspotWalledGardenIp, "src_address", "0.0.0.0"), + resource.TestCheckResourceAttr(testIpHotspotWalledGardenIp, "src_address_list", "slist"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpHotspotWalledGardenIpConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_hotspot_walled_garden_ip" "test" { + action = "reject" + dst_address = "!0.0.0.0" + dst_address_list = "dlist" + dst_port = "0-65535" + protocol = "tcp" + src_address = "0.0.0.0" + src_address_list = "slist" +} +`, providerConfig) +} From 9871047db05a24dac8d265781601a771d1e64878 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 14:18:20 +0300 Subject: [PATCH 09/17] Add a default function for the `Create` and `Update` actions Added `DefaultCreateUpdate` function for resources that change only when the name is specified: set `ftp` disabled=no ports=21 --- routeros/resource_default_actions.go | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/routeros/resource_default_actions.go b/routeros/resource_default_actions.go index 36555fb8..17ac7868 100644 --- a/routeros/resource_default_actions.go +++ b/routeros/resource_default_actions.go @@ -290,3 +290,40 @@ func DefaultSystemDatasourceRead(s map[string]*schema.Schema) schema.ReadContext return MikrotikResourceDataToTerraformDatasource(&[]MikrotikItem{res}, "", s, d) } } + +// FIXME Replace fucntions in resources: ResourceInterfaceEthernetSwitchPortIsolation, ResourceInterfaceEthernetSwitchPort +// ResourceInterfaceEthernetSwitch, ResourceInterfaceLte, ResourceIpService +func DefaultCreateUpdate(s map[string]*schema.Schema) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + item, metadata := TerraformResourceDataToMikrotik(s, d) + + res, err := ReadItems(&ItemId{Name, d.Get("name").(string)}, metadata.Path, m.(Client)) + if err != nil { + // API/REST client error. + ColorizedDebug(ctx, fmt.Sprintf(ErrorMsgPatch, err)) + return diag.FromErr(err) + } + + // Resource not found. + if len(*res) == 0 { + d.SetId("") + ColorizedDebug(ctx, fmt.Sprintf(ErrorMsgPatch, err)) + return diag.FromErr(errorNoLongerExists) + } + + d.SetId((*res)[0].GetID(Id)) + item[".id"] = d.Id() + + var resUrl string + if m.(Client).GetTransport() == TransportREST { + resUrl = "/set" + } + + err = m.(Client).SendRequest(crudPost, &URL{Path: metadata.Path + resUrl}, item, nil) + if err != nil { + return diag.FromErr(err) + } + + return ResourceRead(ctx, s, d, m) + } +} From 153bf68723a1def279431593ed5f5cc2bf2f1ddc Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 14:19:31 +0300 Subject: [PATCH 10/17] feat(hotspot): Add new resource `routeros_ip_hotspot_service_port` --- .../import.sh | 3 ++ .../resource.tf | 4 ++ routeros/provider.go | 1 + routeros/resource_ip_hotspot_service_port.go | 42 +++++++++++++++ .../resource_ip_hotspot_service_port_test.go | 54 +++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 examples/resources/routeros_ip_hotspot_service_port/import.sh create mode 100644 examples/resources/routeros_ip_hotspot_service_port/resource.tf create mode 100644 routeros/resource_ip_hotspot_service_port.go create mode 100644 routeros/resource_ip_hotspot_service_port_test.go diff --git a/examples/resources/routeros_ip_hotspot_service_port/import.sh b/examples/resources/routeros_ip_hotspot_service_port/import.sh new file mode 100644 index 00000000..9949cf75 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_service_port/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/hotspot/service-port get [print show-ids]] +terraform import routeros_ip_hotspot_service_port.test *1 \ No newline at end of file diff --git a/examples/resources/routeros_ip_hotspot_service_port/resource.tf b/examples/resources/routeros_ip_hotspot_service_port/resource.tf new file mode 100644 index 00000000..33779448 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_service_port/resource.tf @@ -0,0 +1,4 @@ +resource "routeros_ip_hotspot_service_port" "test" { + name = "ftp" + disabled = true +} diff --git a/routeros/provider.go b/routeros/provider.go index de90aa3b..4076a675 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -99,6 +99,7 @@ func Provider() *schema.Provider { "routeros_ip_firewall_mangle": ResourceIPFirewallMangle(), "routeros_ip_firewall_nat": ResourceIPFirewallNat(), "routeros_ip_firewall_raw": ResourceIPFirewallRaw(), + "routeros_ip_hotspot_service_port": ResourceIpHotspotServicePort(), "routeros_ip_hotspot_walled_garden": ResourceIpHotspotWalledGarden(), "routeros_ip_hotspot_walled_garden_ip": ResourceIpHotspotWalledGardenIp(), "routeros_ip_neighbor_discovery_settings": ResourceIpNeighborDiscoverySettings(), diff --git a/routeros/resource_ip_hotspot_service_port.go b/routeros/resource_ip_hotspot_service_port.go new file mode 100644 index 00000000..ff7694e5 --- /dev/null +++ b/routeros/resource_ip_hotspot_service_port.go @@ -0,0 +1,42 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +/* + { + ".id": "*1", + "disabled": "false", + "name": "ftp", + "ports": "21" + } +*/ + +func ResourceIpHotspotServicePort() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/hotspot/service-port"), + MetaId: PropId(Id), + MetaSkipFields: PropSkipFields("name"), + + KeyDisabled: PropDisabledRw, + KeyName: PropName("Service name."), + "ports": { + Type: schema.TypeString, + Computed: true, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreateUpdate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultCreateUpdate(resSchema), + DeleteContext: DefaultSystemDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_hotspot_service_port_test.go b/routeros/resource_ip_hotspot_service_port_test.go new file mode 100644 index 00000000..7ccdacf4 --- /dev/null +++ b/routeros/resource_ip_hotspot_service_port_test.go @@ -0,0 +1,54 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpHotspotServicePort = "routeros_ip_hotspot_service_port.test" + +func TestAccIpHotspotServicePortTest_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: testAccIpHotspotServicePortConfig("true"), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpHotspotServicePort), + resource.TestCheckResourceAttr(testIpHotspotServicePort, "disabled", "true"), + resource.TestCheckResourceAttr(testIpHotspotServicePort, "name", "ftp"), + ), + }, + { + Config: testAccIpHotspotServicePortConfig("false"), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpHotspotServicePort), + resource.TestCheckResourceAttr(testIpHotspotServicePort, "disabled", "false"), + resource.TestCheckResourceAttr(testIpHotspotServicePort, "name", "ftp"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpHotspotServicePortConfig(param string) string { + return fmt.Sprintf(`%v + +resource "routeros_ip_hotspot_service_port" "test" { + name = "ftp" + disabled = %v +} +`, providerConfig, param) +} From f2e27b4732f863400c6f6c7f8341315019623f47 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 14:31:17 +0300 Subject: [PATCH 11/17] feat(hotspot): Add new resource `routeros_ip_hotspot_ip_binding` --- .../routeros_ip_hotspot_ip_binding/import.sh | 3 + .../resource.tf | 6 ++ routeros/provider.go | 1 + routeros/resource_ip_hotspot_ip_binding.go | 70 +++++++++++++++++++ .../resource_ip_hotspot_ip_binding_test.go | 51 ++++++++++++++ 5 files changed, 131 insertions(+) create mode 100755 examples/resources/routeros_ip_hotspot_ip_binding/import.sh create mode 100755 examples/resources/routeros_ip_hotspot_ip_binding/resource.tf create mode 100644 routeros/resource_ip_hotspot_ip_binding.go create mode 100644 routeros/resource_ip_hotspot_ip_binding_test.go diff --git a/examples/resources/routeros_ip_hotspot_ip_binding/import.sh b/examples/resources/routeros_ip_hotspot_ip_binding/import.sh new file mode 100755 index 00000000..ceefa2af --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_ip_binding/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/hotspot/ip-binding get [print show-ids]] +terraform import routeros_ip_hotspot_ip_binding.test *3 \ No newline at end of file diff --git a/examples/resources/routeros_ip_hotspot_ip_binding/resource.tf b/examples/resources/routeros_ip_hotspot_ip_binding/resource.tf new file mode 100755 index 00000000..213ef325 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_ip_binding/resource.tf @@ -0,0 +1,6 @@ +resource "routeros_ip_hotspot_ip_binding" "test" { + address = "0.0.0.1" + comment = "comment" + mac_address = "00:00:00:00:01:10" + to_address = "0.0.0.2" +} diff --git a/routeros/provider.go b/routeros/provider.go index 4076a675..986e9750 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -99,6 +99,7 @@ func Provider() *schema.Provider { "routeros_ip_firewall_mangle": ResourceIPFirewallMangle(), "routeros_ip_firewall_nat": ResourceIPFirewallNat(), "routeros_ip_firewall_raw": ResourceIPFirewallRaw(), + "routeros_ip_hotspot_ip_binding": ResourceIpHotspotIpBinding(), "routeros_ip_hotspot_service_port": ResourceIpHotspotServicePort(), "routeros_ip_hotspot_walled_garden": ResourceIpHotspotWalledGarden(), "routeros_ip_hotspot_walled_garden_ip": ResourceIpHotspotWalledGardenIp(), diff --git a/routeros/resource_ip_hotspot_ip_binding.go b/routeros/resource_ip_hotspot_ip_binding.go new file mode 100644 index 00000000..a90c933b --- /dev/null +++ b/routeros/resource_ip_hotspot_ip_binding.go @@ -0,0 +1,70 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*1", + "address": "0.0.0.1", + "comment": "comment", + "disabled": "false", + "mac-address": "00:00:00:00:01:10", + "to-address": "0.0.0.2" + } +*/ + +// https://help.mikrotik.com/docs/pages/viewpage.action?pageId=56459266#HotSpot(Captiveportal)-IPBinding +func ResourceIpHotspotIpBinding() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/hotspot/ip-binding"), + MetaId: PropId(Id), + + "address": { + Type: schema.TypeString, + Optional: true, + Description: "The original IP address of the client.", + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "mac_address": { + Type: schema.TypeString, + Optional: true, + Description: "MAC address of the client.", + }, + "server": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the HotSpot server. `all` - will be applied to all hotspot servers.", + }, + "to_address": { + Type: schema.TypeString, + Optional: true, + Description: "New IP address of the client, translation occurs on the router (client does not know anything " + + "about the translation).", + }, + "type": { + Type: schema.TypeString, + Optional: true, + Description: "Type of the IP-binding action `regular` - performs One-to-One NAT according to the rule, translates " + + "the address to to-address; `bypassed` - performs the translation, but excludes client from login to the " + + "HotSpot; `blocked` - translation is not performed and packets from a host are dropped.", + ValidateFunc: validation.StringInSlice([]string{"blocked", "bypassed", "regular"}, 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_ip_hotspot_ip_binding_test.go b/routeros/resource_ip_hotspot_ip_binding_test.go new file mode 100644 index 00000000..f2ff6eeb --- /dev/null +++ b/routeros/resource_ip_hotspot_ip_binding_test.go @@ -0,0 +1,51 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpHotspotIpBinding = "routeros_ip_hotspot_ip_binding.test" + +func TestAccIpHotspotIpBindingTest_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, + CheckDestroy: testCheckResourceDestroy("/ip/hotspot/ip-binding", "routeros_ip_hotspot_ip_binding"), + Steps: []resource.TestStep{ + { + Config: testAccIpHotspotIpBindingConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpHotspotIpBinding), + resource.TestCheckResourceAttr(testIpHotspotIpBinding, "address", "0.0.0.1"), + resource.TestCheckResourceAttr(testIpHotspotIpBinding, "comment", "comment"), + resource.TestCheckResourceAttr(testIpHotspotIpBinding, "mac_address", "00:00:00:00:01:10"), + resource.TestCheckResourceAttr(testIpHotspotIpBinding, "to_address", "0.0.0.2"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpHotspotIpBindingConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_hotspot_ip_binding" "test" { + address = "0.0.0.1" + comment = "comment" + mac_address = "00:00:00:00:01:10" + to_address = "0.0.0.2" +} +`, providerConfig) +} From 4de2db943c82de89e2081b8b32ffed8621f42ab9 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 16:15:45 +0300 Subject: [PATCH 12/17] feat(hotspot): Add new resource `routeros_ip_hotspot_user_profile` --- .../import.sh | 3 + .../resource.tf | 12 + routeros/provider.go | 1 + routeros/resource_ip_hotspot_user_profile.go | 242 ++++++++++++++++++ .../resource_ip_hotspot_user_profile_test.go | 54 ++++ 5 files changed, 312 insertions(+) create mode 100644 examples/resources/routeros_ip_hotspot_user_profile/import.sh create mode 100644 examples/resources/routeros_ip_hotspot_user_profile/resource.tf create mode 100644 routeros/resource_ip_hotspot_user_profile.go create mode 100644 routeros/resource_ip_hotspot_user_profile_test.go diff --git a/examples/resources/routeros_ip_hotspot_user_profile/import.sh b/examples/resources/routeros_ip_hotspot_user_profile/import.sh new file mode 100644 index 00000000..7a491510 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_user_profile/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/hotspot/user/profile get [print show-ids]] +terraform import routeros_ip_hotspot_user_profile.test *3 \ No newline at end of file diff --git a/examples/resources/routeros_ip_hotspot_user_profile/resource.tf b/examples/resources/routeros_ip_hotspot_user_profile/resource.tf new file mode 100644 index 00000000..65d1bd70 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_user_profile/resource.tf @@ -0,0 +1,12 @@ +resource "routeros_ip_hotspot_user_profile" "test" { + add_mac_cookie = true + address_list = "list-1" + idle_timeout = "none" + keepalive_timeout = "2m" + mac_cookie_timeout = "3d" + name = "new-profile" + shared_users = 3 + status_autorefresh = "2m" + transparent_proxy = true + advertise = true +} diff --git a/routeros/provider.go b/routeros/provider.go index 986e9750..27b35de7 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -101,6 +101,7 @@ func Provider() *schema.Provider { "routeros_ip_firewall_raw": ResourceIPFirewallRaw(), "routeros_ip_hotspot_ip_binding": ResourceIpHotspotIpBinding(), "routeros_ip_hotspot_service_port": ResourceIpHotspotServicePort(), + "routeros_ip_hotspot_user_profile": ResourceIpHotspotUserProfile(), "routeros_ip_hotspot_walled_garden": ResourceIpHotspotWalledGarden(), "routeros_ip_hotspot_walled_garden_ip": ResourceIpHotspotWalledGardenIp(), "routeros_ip_neighbor_discovery_settings": ResourceIpNeighborDiscoverySettings(), diff --git a/routeros/resource_ip_hotspot_user_profile.go b/routeros/resource_ip_hotspot_user_profile.go new file mode 100644 index 00000000..2866d570 --- /dev/null +++ b/routeros/resource_ip_hotspot_user_profile.go @@ -0,0 +1,242 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*1", + "add-mac-cookie": "true", + "address-list": "aaa", + "advertise": "true", + "advertise-interval": "30m,10m", + "advertise-timeout": "immediately", + "advertise-url": "https://www.mikrotik.com/", + "idle-timeout": "none", + "incoming-filter": "bbb", + "incoming-packet-mark": "ddd", + "insert-queue-before": "first", + "keepalive-timeout": "2m", + "mac-cookie-timeout": "3d", + "name": "uprof1", + "on-login": "s1", + "on-logout": "s2", + "open-status-page": "always", + "outgoing-filter": "ccc", + "outgoing-packet-mark": "eee", + "parent-queue": "none", + "queue-type": "default-small", + "rate-limit": "1", + "session-timeout": "1s", + "shared-users": "1", + "status-autorefresh": "1m", + "transparent-proxy": "true" + } +*/ + +// https://wiki.mikrotik.com/wiki/Manual:IP/Hotspot/User#User_Profile +func ResourceIpHotspotUserProfile() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/hotspot/user/profile"), + MetaId: PropId(Id), + MetaSetUnsetFields: PropSetUnsetFields("insert_queue_before", "parent_queue", "queue_type"), + + "add_mac_cookie": { + Type: schema.TypeBool, + Optional: true, + Description: "Allows to add mac cookie for users.", + }, + "address_list": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the address list in which users IP address will be added. Useful to mark traffic per " + + "user groups for queue tree configurations.", + }, + "address_pool": { + Type: schema.TypeString, + Optional: true, + Description: "IP pool name from which the user will get IP. When user has improper network settings configuration " + + "on the computer, HotSpot server makes translation and assigns correct IP address from the pool instead " + + "of incorrect one.", + }, + "advertise": { + Type: schema.TypeBool, + Optional: true, + Description: "Enable forced advertisement popups. After certain interval specific web-page is being displayed " + + "for HotSpot users. Advertisement page might be blocked by browsers popup blockers.", + }, + "advertise_interval": { + Type: schema.TypeSet, + Optional: true, + Description: "Set of intervals between advertisement popups. After the list is done, the last value is used " + + "for all further advertisements, 10 minutes.", + Elem: &schema.Schema{ + Type: schema.TypeString, + DiffSuppressFunc: TimeEquall, + }, + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "advertise_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "How long advertisement is shown, before blocking network access for HotSpot client. Connection " + + "to Internet is not allowed, when advertisement is not shown.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "advertise_url": { + Type: schema.TypeString, + Optional: true, + Description: "List of URLs that is show for advertisement popups. After the last URL is used, list starts " + + "from the begining.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "default": { + Type: schema.TypeBool, + Computed: true, + Description: "It's the default rule.", + }, + "idle_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "Maximal period of inactivity for authorized HotSpot clients. Timer is counting, when there " + + "is no traffic coming from that client and going through the router, for example computer is switched " + + "off. User is logged out, dropped of the host list, the address used by the user is freed, when timeout " + + "is reached.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "incoming_filter": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the firewall chain applied to incoming packets from the users of this profile, jump " + + "rule is required from built-in chain (input, forward, output) to chain=hotspot.", + }, + "incoming_packet_mark": { + Type: schema.TypeString, + Optional: true, + Description: "Packet mark put on incoming packets from every user of this profile.", + }, + "insert_queue_before": { + Type: schema.TypeString, + Optional: true, + Description: "", + ValidateFunc: validation.StringInSlice([]string{"first", "bottom"}, false), + }, + "keepalive_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "Keepalive timeout for authorized HotSpot clients. Used to detect, that the computer of the " + + "client is alive and reachable. User is logged out, when timeout value is reached.", + DiffSuppressFunc: TimeEquall, + }, + "mac_cookie_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "Selects mac-cookie timeout from last login or logout. Read more>>.", + DiffSuppressFunc: TimeEquall, + }, + KeyName: PropName("Descriptive name of the profile."), + "on_login": { + Type: schema.TypeString, + Optional: true, + Description: "Script name to be executed, when user logs in to the HotSpot from the particular profile. " + + "It is possible to get username from internal user and interface variable. For example, :log info ``User " + + "$user logged in!`` . If hotspot is set on bridge interface, then interface variable will show bridge " + + "as actual interface unless use-ip-firewall' is set in bridge settings. List of available variables: " + + "$user $username (alternative var name for $user) $address $``mac-address`` $interface.", + }, + "on_logout": { + Type: schema.TypeString, + Optional: true, + Description: "Script name to be executed, when user logs out from the HotSpot.It is possible to get username " + + "from internal user and interface variable. For example, :log info ``User $user logged in!`` . If hotspot " + + "is set on bridge interface, then interface variable will show bridge as actual interface unless use-ip-firewall " + + "is set in bridge settings. List of available variables: $user $username (alternative var name for $user) " + + "$address $``mac-address`` $interface $cause Starting with v6.34rc11 some additional variables are available: " + + "$uptime-secs - final session time in seconds $bytes-in - bytes uploaded $bytes-out - bytes downloaded " + + "$bytes-total - bytes up + bytes down $packets-in - packets uploaded $packets-out - packets downloaded " + + "$packets-total - packets up + packets down.", + }, + "open_status_page": { + Type: schema.TypeString, + Optional: true, + Description: "Option to show status page for user authenticated with mac login method. For example to show " + + "advertisement on status page (alogin.html) http-login - open status page only for HTTP login (includes " + + "cookie and HTTPS) always - open HTTP status page in case of mac login as well.", + ValidateFunc: validation.StringInSlice([]string{"always", "http-login"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "outgoing_filter": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the firewall chain applied to outgoing packets from the users of this profile, jump " + + "rule is required from built-in chain (input, forward, output) to chain=hotspot.", + }, + "outgoing_packet_mark": { + Type: schema.TypeString, + Optional: true, + Description: "Packet mark put on outgoing packets from every user of this profile.", + }, + "parent_queue": { + Type: schema.TypeString, + Optional: true, + Description: "", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "queue_type": { + Type: schema.TypeString, + Optional: true, + Description: "", + ValidateFunc: validation.StringInSlice([]string{"default", "default-small", "ethernet-default", + "hotspot-default", "multi-queue-ethernet-default", "only-hardware-queue", "pcq-download-default", + "pcq-upload-default", "synchronous-default", "wireless-default"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "rate_limit": { + Type: schema.TypeString, + Optional: true, + Description: "Simple dynamic queue is created for user, once it logs in to the HotSpot. Rate-limitation " + + "is configured in the following form [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]]]]. For example, to set 1M download, " + + "512k upload for the client, rate-limit=512k/1M.", + }, + "session_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "Allowed session time for client. After this time, the user is logged out unconditionally.", + DiffSuppressFunc: TimeEquall, + }, + "shared_users": { + Type: schema.TypeInt, + Optional: true, + Description: "Allowed number of simultaneously logged in users with the same HotSpot username.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "status_autorefresh": { + Type: schema.TypeString, + Optional: true, + Description: "HotSpot status page autorefresh interval.", + DiffSuppressFunc: TimeEquall, + }, + "transparent_proxy": { + Type: schema.TypeBool, + Optional: true, + Description: "Use transparent HTTP proxy for the authorized users of this profile.", + 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_ip_hotspot_user_profile_test.go b/routeros/resource_ip_hotspot_user_profile_test.go new file mode 100644 index 00000000..e59dbdaa --- /dev/null +++ b/routeros/resource_ip_hotspot_user_profile_test.go @@ -0,0 +1,54 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpHotspotUserProfile = "routeros_ip_hotspot_user_profile.test" + +func TestAccIpHotspotUserProfileTest_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, + CheckDestroy: testCheckResourceDestroy("/ip/hotspot/user/profile", "routeros_ip_hotspot_user_profile"), + Steps: []resource.TestStep{ + { + Config: testAccIpHotspotUserProfileConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpHotspotUserProfile), + resource.TestCheckResourceAttr(testIpHotspotUserProfile, "advertise", "true"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpHotspotUserProfileConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_hotspot_user_profile" "test" { + add_mac_cookie = true + address_list = "list-1" + idle_timeout = "none" + keepalive_timeout = "2m" + mac_cookie_timeout = "3d" + name = "new-profile" + shared_users = 3 + status_autorefresh = "2m" + transparent_proxy = true + advertise = true +} +`, providerConfig) +} From b897532232c12245b4c92f27ca129ebe8f6c8d31 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 16:34:46 +0300 Subject: [PATCH 13/17] feat(hotspot): Add new resource `routeros_ip_hotspot_user` --- .../routeros_ip_hotspot_user/import.sh | 3 + .../routeros_ip_hotspot_user/resource.tf | 3 + routeros/provider.go | 1 + routeros/resource_ip_hotspot_user.go | 126 ++++++++++++++++++ routeros/resource_ip_hotspot_user_test.go | 46 +++++++ 5 files changed, 179 insertions(+) create mode 100644 examples/resources/routeros_ip_hotspot_user/import.sh create mode 100644 examples/resources/routeros_ip_hotspot_user/resource.tf create mode 100644 routeros/resource_ip_hotspot_user.go create mode 100644 routeros/resource_ip_hotspot_user_test.go diff --git a/examples/resources/routeros_ip_hotspot_user/import.sh b/examples/resources/routeros_ip_hotspot_user/import.sh new file mode 100644 index 00000000..c3a2df7b --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_user/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/hotspot/user get [print show-ids]] +terraform import routeros_ip_hotspot_user.test *3 \ No newline at end of file diff --git a/examples/resources/routeros_ip_hotspot_user/resource.tf b/examples/resources/routeros_ip_hotspot_user/resource.tf new file mode 100644 index 00000000..50d520f2 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_user/resource.tf @@ -0,0 +1,3 @@ +resource "routeros_ip_hotspot_user" "test" { + name = "user-1" +} \ No newline at end of file diff --git a/routeros/provider.go b/routeros/provider.go index 27b35de7..1e328192 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -101,6 +101,7 @@ func Provider() *schema.Provider { "routeros_ip_firewall_raw": ResourceIPFirewallRaw(), "routeros_ip_hotspot_ip_binding": ResourceIpHotspotIpBinding(), "routeros_ip_hotspot_service_port": ResourceIpHotspotServicePort(), + "routeros_ip_hotspot_user": ResourceIpHotspotUser(), "routeros_ip_hotspot_user_profile": ResourceIpHotspotUserProfile(), "routeros_ip_hotspot_walled_garden": ResourceIpHotspotWalledGarden(), "routeros_ip_hotspot_walled_garden_ip": ResourceIpHotspotWalledGardenIp(), diff --git a/routeros/resource_ip_hotspot_user.go b/routeros/resource_ip_hotspot_user.go new file mode 100644 index 00000000..30ab4349 --- /dev/null +++ b/routeros/resource_ip_hotspot_user.go @@ -0,0 +1,126 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +/* +{ + ".id": "*1", + "address": "0.0.0.1", + "bytes-in": "0", + "bytes-out": "0", + "disabled": "false", + "dynamic": "false", + "email": "mail@g.com", + "limit-bytes-in": "100", + "limit-bytes-out": "200", + "limit-bytes-total": "500", + "limit-uptime": "1m", + "mac-address": "11:00:00:00:00:00", + "name": "user1", + "packets-in": "0", + "packets-out": "0", + "password": "123", + "profile": "default", + "routes": "10.0.0.0/24", + "uptime": "0s" +} +*/ + +// https://wiki.mikrotik.com/wiki/Manual:IP/Hotspot/User#Users +func ResourceIpHotspotUser() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/hotspot/user"), + MetaId: PropId(Id), + MetaSkipFields: PropSkipFields("bytes_in", "bytes_out", "packets_in", "packets_out", "uptime"), + + "address": { + Type: schema.TypeInt, + Optional: true, + Description: "IP address, when specified client will get the address from the HotSpot one-to-one NAT translations. " + + "Address does not restrict HotSpot login only from this address.", + }, + KeyComment: PropCommentRw, + "default": { + Type: schema.TypeBool, + Computed: true, + Description: "It's the default rule.", + }, + KeyDisabled: PropDisabledRw, + KeyDynamic: PropDynamicRo, + "email": { + Type: schema.TypeString, + Optional: true, + Description: "HotSpot client's e-mail, informational value for the HotSpot user.", + }, + "limit_bytes_in": { + Type: schema.TypeInt, + Optional: true, + Description: "Maximal amount of bytes that can be received from the user. User is disconnected from HotSpot " + + "after the limit is reached.", + }, + "limit_bytes_out": { + Type: schema.TypeInt, + Optional: true, + Description: "Maximal amount of bytes that can be transmitted from the user. User is disconnected from HotSpot " + + "after the limit is reached.", + }, + "limit_bytes_total": { + Type: schema.TypeInt, + Optional: true, + Description: "(limit-bytes-in+limit-bytes-out). User is disconnected from HotSpot after the limit is reached.", + }, + "limit_uptime": { + Type: schema.TypeInt, + Optional: true, + Description: "Uptime limit for the HotSpot client, user is disconnected from HotSpot as soon as uptime is " + + "reached.", + }, + "mac_address": { + Type: schema.TypeInt, + Optional: true, + Description: "Client is allowed to login only from the specified MAC-address. If value is 00:00:00:00:00:00, " + + "any mac address is allowed.", + }, + KeyName: PropName("HotSpot login page username, when MAC-address authentication is used name is configured as " + + "client's MAC-address."), + "password": { + Type: schema.TypeString, + Optional: true, + Description: "User password.", + Sensitive: true, + }, + "profile": { + Type: schema.TypeString, + Optional: true, + Description: "User profile configured in `/ip hotspot user profile`.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "routes": { + Type: schema.TypeString, + Optional: true, + Description: "Routes added to HotSpot gateway when client is connected. The route format dst-address gateway " + + "metric (for example, `192.168.1.0/24 192.168.0.1 1`).", + }, + "server": { + Type: schema.TypeString, + Optional: true, + Description: "HotSpot server's name to which user is allowed login.", + 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_ip_hotspot_user_test.go b/routeros/resource_ip_hotspot_user_test.go new file mode 100644 index 00000000..336ec102 --- /dev/null +++ b/routeros/resource_ip_hotspot_user_test.go @@ -0,0 +1,46 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpHotspotUser = "routeros_ip_hotspot_user.test" + +func TestAccIpHotspotUserTest_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, + CheckDestroy: testCheckResourceDestroy("/ip/hotspot/user", "routeros_ip_hotspot_user"), + Steps: []resource.TestStep{ + { + Config: testAccIpHotspotUserConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpHotspotUser), + resource.TestCheckResourceAttr(testIpHotspotUser, "name", "user-1"), + resource.TestCheckResourceAttr(testIpHotspotUser, "profile", "default"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpHotspotUserConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_hotspot_user" "test" { + name = "user-1" +} +`, providerConfig) +} From 06b974bdfbd6508a3f32a6b52456f0c8a6ba10b0 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 19:06:31 +0300 Subject: [PATCH 14/17] feat(hotspot): Add new resource `routeros_ip_hotspot_profile` --- .../routeros_ip_hotspot_profile/import.sh | 3 + .../routeros_ip_hotspot_profile/resource.tf | 5 + routeros/provider.go | 1 + routeros/resource_ip_hotspot_profile.go | 237 ++++++++++++++++++ routeros/resource_ip_hotspot_profile_test.go | 51 ++++ 5 files changed, 297 insertions(+) create mode 100755 examples/resources/routeros_ip_hotspot_profile/import.sh create mode 100755 examples/resources/routeros_ip_hotspot_profile/resource.tf create mode 100644 routeros/resource_ip_hotspot_profile.go create mode 100644 routeros/resource_ip_hotspot_profile_test.go diff --git a/examples/resources/routeros_ip_hotspot_profile/import.sh b/examples/resources/routeros_ip_hotspot_profile/import.sh new file mode 100755 index 00000000..72a36f00 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_profile/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/hotspot/profile get [print show-ids]] +terraform import routeros_ip_hotspot_profile.test *3 \ No newline at end of file diff --git a/examples/resources/routeros_ip_hotspot_profile/resource.tf b/examples/resources/routeros_ip_hotspot_profile/resource.tf new file mode 100755 index 00000000..f19c1f6b --- /dev/null +++ b/examples/resources/routeros_ip_hotspot_profile/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_ip_hotspot_profile" "test" { + name = "hsprof-1" + login_by = ["mac", "https", "trial"] + use_radius = true +} diff --git a/routeros/provider.go b/routeros/provider.go index 1e328192..4ad456ef 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -100,6 +100,7 @@ func Provider() *schema.Provider { "routeros_ip_firewall_nat": ResourceIPFirewallNat(), "routeros_ip_firewall_raw": ResourceIPFirewallRaw(), "routeros_ip_hotspot_ip_binding": ResourceIpHotspotIpBinding(), + "routeros_ip_hotspot_profile": ResourceIpHotspotProfile(), "routeros_ip_hotspot_service_port": ResourceIpHotspotServicePort(), "routeros_ip_hotspot_user": ResourceIpHotspotUser(), "routeros_ip_hotspot_user_profile": ResourceIpHotspotUserProfile(), diff --git a/routeros/resource_ip_hotspot_profile.go b/routeros/resource_ip_hotspot_profile.go new file mode 100644 index 00000000..cbfd3378 --- /dev/null +++ b/routeros/resource_ip_hotspot_profile.go @@ -0,0 +1,237 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*2", + "dns-name": "", + "hotspot-address": "192.168.11.1", + "html-directory": "hotspot", + "html-directory-override": "", + "http-cookie-lifetime": "3d", + "http-proxy": "0.0.0.0:0", + "install-hotspot-queue": "false", + "login-by": "cookie,http-chap,https", + "name": "hsprof2", + "smtp-server": "0.0.0.0", + "split-user-domain": "false", + "ssl-certificate": "tls", + "use-radius": "false" + } +*/ + +// https://wiki.mikrotik.com/wiki/Manual:IP/Hotspot/Profile +func ResourceIpHotspotProfile() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/hotspot/profile"), + MetaId: PropId(Id), + + "dns_name": { + Type: schema.TypeString, + Optional: true, + Description: "DNS name of the HotSpot server (it appears as the location of the login page). This name will " + + "automatically be added as a static DNS entry in the DNS cache. Name can affect if Hotspot is automatically " + + "detected by client device. For example, iOS devices may not detect Hotspot that has a name which includes " + + "`.local`.", + }, + "hotspot_address": { + Type: schema.TypeString, + Optional: true, + Description: "IP address of HotSpot service.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "html_directory": { + Type: schema.TypeString, + Optional: true, + Description: "Directory name in which HotSpot HTML pages are stored (by default hotspot directory). It is " + + "possible to specify different directory with modified HTML pages. To change HotSpot login page, connect " + + "to the router with FTP and download hotspot directory contents. v6.31 and older software builds: For " + + "devices where `flash` directory is present, hotspot html directory must be stored there and path must " + + "be typed in as follows: `/(hotspot_dir)`. This must be done in this order as hotspot sees `flash` " + + "directory as root location. v6.32 and newer software builds: full path must be typed in html-directory " + + "field, including `/flash/(hotspot_dir)`.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "html_directory_override": { + Type: schema.TypeString, + Optional: true, + Description: "Alternative path for hotspot html files. It should be used only if customized hotspot html " + + "files are stored on external storage(attached usb, hdd, etc). If configured then hotspot will switch " + + "to this html path as soon at it becomes available and switch back to html-directory path if override " + + "path becomes non-available for some reason.", + }, + "http_cookie_lifetime": { + Type: schema.TypeString, + Optional: true, + Description: "HTTP cookie validity time, the option is related to cookie HotSpot login method.", + DiffSuppressFunc: TimeEquall, + }, + "http_proxy": { + Type: schema.TypeString, + Optional: true, + Description: "Address and port of the proxy server for HotSpot service, when default value is used all request " + + "are resolved by the local `/ip proxy`.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "https_redirect": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to redirect unauthenticated user to hotspot login page, if he is visiting a https:// " + + "url. Since certificate domain name will mismatch, often this leads to errors, so you can set this parameter " + + "to `no` and all https requests will simply be rejected and user will have to visit a http page.", + }, + "login_by": { + Type: schema.TypeSet, + Optional: true, + Description: "Used HotSpot authentication method\n" + + "* mac-cookie - enables login by mac cookie method.\n" + + "* cookie - may only be used with other HTTP authentication method. HTTP cookie is generated, when user authenticates " + + "in HotSpot for the first time. User is not asked for the login/password and authenticated automatically, " + + "until cookie-lifetime is active.\n" + + "* http-chap - login/password is required for the user to authenticate in HotSpot. CHAP " + + "challenge-response method with MD5 hashing algorithm is used for protecting passwords. \n" + + "* http-pap - login/password is required for user to authenticate in HotSpot. Username and password are " + + "sent over network in plain text.\n" + + "* https - login/password is required for user to authenticate in HotSpot. Client login/password " + + "exchange between client and server is encrypted with SSL tunnel.\n" + + "* mac - client is authenticated without asking login form. Client MAC-address is added to `/ip hotspot " + + "user` database, client is authenticated as soon as connected to the HotSpot\n" + + "* trial - client is allowed to use internet without HotSpot login for the specified amount of time.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"cookie", "http-chap", "http-pap", "https", "mac", + "trial", "mac-cookie"}, false), + }, + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "mac_auth_mode": { + Type: schema.TypeString, + Optional: true, + Description: "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), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "mac_auth_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Used together with MAC authentication, field used to specify password for the users to be " + + "authenticated by their MAC addresses. The following option is required, when specific RADIUS server " + + "rejects authentication for the clients with blank password.", + }, + KeyName: PropName("Descriptive name of the profile."), + "nas_port_type": { + Type: schema.TypeString, + Optional: true, + Description: "`NAS-Port-Type` value to be sent to RADIUS server, `NAS-Port-Type` values are described in the " + + "RADIUS RFC 2865. This optional value attribute indicates the type of the physical port of the HotSpot " + + "server.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "radius_accounting": { + Type: schema.TypeBool, + Optional: true, + Description: "Send RADIUS server accounting information for each user, when yes is used.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "radius_default_domain": { + Type: schema.TypeString, + Optional: true, + Description: "Default domain to use for RADIUS requests. Allows to use separate RADIUS server per `/ip hotspot " + + "profile`. If used, same domain name should be specified under `/radius domain` value.", + }, + "radius_interim_update": { + Type: schema.TypeString, + Optional: true, + Description: "How often to send accounting updates . When received is set, interim-time is used from RADIUS " + + "server. 0s is the same as received.", + DiffSuppressFunc: TimeEquall, + }, + "radius_location_name": { + Type: schema.TypeString, + Optional: true, + Description: "`RADIUS-Location-Id` to be sent to RADIUS server. Used to identify location of the HotSpot server " + + "during the communication with RADIUS server. Value is optional and used together with RADIUS server.", + }, + "radius_mac_format": { + Type: schema.TypeString, + Optional: true, + Description: "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", + "XXXXXX-XXXXXX", "XXXXXXXXXXXX", "XX-XX-XX-XX-XX-XX", "XXXX:XXXX:XXXX", "XXXXXX:XXXXXX"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "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 should be numbers with " + + "optional 'k' (1,000s) or 'M' (1,000,000s). If tx-rate is not specified, rx-rate is as tx-rate too. Same " + + "goes for tx-burst-rate and 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 is used as burst thresholds. If " + + "both rx-burst-time and tx-burst-time are not specified, 1s is used as default. rx-rate-min and tx-rate " + + "min are the values of limit-at properties.", + }, + "smtp_server": { + Type: schema.TypeString, + Optional: true, + Description: "SMTP server address to be used to redirect HotSpot users SMTP requests.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "split_user_domain": { + Type: schema.TypeBool, + Optional: true, + Description: "Split username from domain name when the username is given in `user@domain` or in `domain\\user` " + + "format from RADIUS server.", + }, + "ssl_certificate": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the SSL certificate on the router to to use only for HTTPS authentication.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "trial_uptime_limit": { + Type: schema.TypeString, + Optional: true, + Description: "Used only with trial authentication method. Time value specifies, how long trial user " + + "identified by MAC address can use access to public networks without HotSpot authentication.", + DiffSuppressFunc: TimeEquall, + }, + "trial_uptime_reset": { + Type: schema.TypeString, + Optional: true, + Description: "Used only with trial authentication method.", + DiffSuppressFunc: TimeEquall, + }, + "trial_user_profile": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies hotspot user profile for trial users.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "use_radius": { + Type: schema.TypeBool, + Optional: true, + Description: "Use RADIUS to authenticate HotSpot users.", + }, + } + + 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_ip_hotspot_profile_test.go b/routeros/resource_ip_hotspot_profile_test.go new file mode 100644 index 00000000..625a40d4 --- /dev/null +++ b/routeros/resource_ip_hotspot_profile_test.go @@ -0,0 +1,51 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpHotspotProfile = "routeros_ip_hotspot_profile.test" + +func TestAccIpHotspotProfileTest_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, + CheckDestroy: testCheckResourceDestroy("/ip/hotspot/profile", "routeros_ip_hotspot_profile"), + Steps: []resource.TestStep{ + { + Config: testAccIpHotspotProfileConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpHotspotProfile), + resource.TestCheckResourceAttr(testIpHotspotProfile, "name", "hsprof-1"), + resource.TestCheckResourceAttr(testIpHotspotProfile, "login_by.0", "https"), + resource.TestCheckResourceAttr(testIpHotspotProfile, "login_by.1", "mac"), + resource.TestCheckResourceAttr(testIpHotspotProfile, "login_by.2", "trial"), + resource.TestCheckResourceAttr(testIpHotspotProfile, "use_radius", "true"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpHotspotProfileConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_hotspot_profile" "test" { + name = "hsprof-1" + login_by = ["mac", "https", "trial"] + use_radius = true +} +`, providerConfig) +} From 1da8f3d755ec0a35836b67037d57e40d27d3453f Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 19:19:40 +0300 Subject: [PATCH 15/17] feat(hotspot): Add new resource `routeros_ip_hotspot` --- .../resources/routeros_ip_hotspot/import.sh | 3 + .../resources/routeros_ip_hotspot/resource.tf | 4 + routeros/provider.go | 1 + routeros/resource_ip_hotspot.go | 95 +++++++++++++++++++ routeros/resource_ip_hotspot_test.go | 47 +++++++++ 5 files changed, 150 insertions(+) create mode 100644 examples/resources/routeros_ip_hotspot/import.sh create mode 100644 examples/resources/routeros_ip_hotspot/resource.tf create mode 100644 routeros/resource_ip_hotspot.go create mode 100644 routeros/resource_ip_hotspot_test.go diff --git a/examples/resources/routeros_ip_hotspot/import.sh b/examples/resources/routeros_ip_hotspot/import.sh new file mode 100644 index 00000000..8ef4d461 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/hotspot get [print show-ids]] +terraform import routeros_ip_hotspot.test *3 \ No newline at end of file diff --git a/examples/resources/routeros_ip_hotspot/resource.tf b/examples/resources/routeros_ip_hotspot/resource.tf new file mode 100644 index 00000000..ee601fa2 --- /dev/null +++ b/examples/resources/routeros_ip_hotspot/resource.tf @@ -0,0 +1,4 @@ +resource "routeros_ip_hotspot" "test" { + name = "server-1" + interface = "ether2" +} diff --git a/routeros/provider.go b/routeros/provider.go index 4ad456ef..52dd3de2 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -99,6 +99,7 @@ func Provider() *schema.Provider { "routeros_ip_firewall_mangle": ResourceIPFirewallMangle(), "routeros_ip_firewall_nat": ResourceIPFirewallNat(), "routeros_ip_firewall_raw": ResourceIPFirewallRaw(), + "routeros_ip_hotspot": ResourceIpHotspot(), "routeros_ip_hotspot_ip_binding": ResourceIpHotspotIpBinding(), "routeros_ip_hotspot_profile": ResourceIpHotspotProfile(), "routeros_ip_hotspot_service_port": ResourceIpHotspotServicePort(), diff --git a/routeros/resource_ip_hotspot.go b/routeros/resource_ip_hotspot.go new file mode 100644 index 00000000..11f0a750 --- /dev/null +++ b/routeros/resource_ip_hotspot.go @@ -0,0 +1,95 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +/* + { + ".id": "*5", + "HTTPS": "false", + "addresses-per-mac": "unlimited", + "disabled": "false", + "idle-timeout": "5m", + "interface": "ether4", + "invalid": "false", + "keepalive-timeout": "none", + "login-timeout": "none", + "name": "server1", + "profile": "default", + "proxy-status": "running" + } +*/ + +// https://help.mikrotik.com/docs/pages/viewpage.action?pageId=56459266#HotSpot(Captiveportal)-IPHotSpot +func ResourceIpHotspot() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/hotspot"), + MetaId: PropId(Id), + MetaSkipFields: PropSkipFields("HTTPS", "keepalive-timeout", "proxy_status"), + + "address_pool": { + Type: schema.TypeString, + Optional: true, + Description: "Address space used to change HotSpot client any IP address to a valid address. Useful for " + + "providing public network access to mobile clients that are not willing to change their networking settings.", + }, + "addresses_per_mac": { + Type: schema.TypeString, + Optional: true, + Description: "Number of IP addresses allowed to be bind with the MAC address, when multiple HotSpot clients " + + "connected with one MAC-address.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + KeyDisabled: PropDisabledRw, + "idle_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "Period of inactivity for unauthorized clients. When there is no traffic from this client (literally " + + "client computer should be switched off), once the timeout is reached, a user is dropped from the HotSpot " + + "host list, its used address becomes available.", + DiffSuppressFunc: TimeEquall, + }, + "interface": { + Type: schema.TypeString, + Required: true, + Description: "Interface to run HotSpot on.", + }, + KeyInvalid: PropInvalidRo, + "keepalive_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "The exact value of the keepalive-timeout, that is applied to the user. Value shows how long " + + "the host can stay out of reach to be removed from the HotSpot.", + DiffSuppressFunc: TimeEquall, + }, + "login_timeout": { + Type: schema.TypeString, + Optional: true, + Description: "Period of time after which if a host hasn't been authorized itself with a system the host " + + "entry gets deleted from host table. Loop repeats until the host logs in the system. Enable if there " + + "are situations where a host cannot log in after being too long in the host table unauthorized.", + DiffSuppressFunc: TimeEquall, + }, + KeyName: PropName("HotSpot server's name or identifier."), + "profile": { + Type: schema.TypeString, + Optional: true, + Description: "HotSpot server default HotSpot profile, which is located in `/ip/hotspot/profile`.", + 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_ip_hotspot_test.go b/routeros/resource_ip_hotspot_test.go new file mode 100644 index 00000000..b628b566 --- /dev/null +++ b/routeros/resource_ip_hotspot_test.go @@ -0,0 +1,47 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpHotspot = "routeros_ip_hotspot.test" + +func TestAccIpHotspotTest_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, + CheckDestroy: testCheckResourceDestroy("/ip/hotspot", "routeros_ip_hotspot"), + Steps: []resource.TestStep{ + { + Config: testAccIpHotspotConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpHotspot), + resource.TestCheckResourceAttr(testIpHotspot, "name", "server-1"), + resource.TestCheckResourceAttr(testIpHotspot, "interface", "ether2"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpHotspotConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_hotspot" "test" { + name = "server-1" + interface = "ether2" +} +`, providerConfig) +} From e4c3050bc8257f9da5604991e4e075af8a68f17b Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 19:44:07 +0300 Subject: [PATCH 16/17] test: Add network interface printing --- .github/scripts/setup_routeros.go | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/scripts/setup_routeros.go b/.github/scripts/setup_routeros.go index 263ce9cd..6a08dbc5 100644 --- a/.github/scripts/setup_routeros.go +++ b/.github/scripts/setup_routeros.go @@ -19,6 +19,7 @@ var ( "/ip/pool/add name=dhcp ranges=192.168.88.100-192.168.88.200", "/interface/wireguard/add name=wg1", "/interface/list/add name=list", + "/interface/print", } ) From 22d333cbe44b96754ff98d85fef03d686c127282 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 24 Sep 2024 20:17:49 +0300 Subject: [PATCH 17/17] test: Change the interface for the test. Interface `ether2` is renamed to `terraform` within the `TestAccInterfaceEthernetTest_basic` test framework --- routeros/resource_ip_hotspot_test.go | 4 ++-- routeros/resource_tool_sniffer_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/routeros/resource_ip_hotspot_test.go b/routeros/resource_ip_hotspot_test.go index b628b566..b118d8a1 100644 --- a/routeros/resource_ip_hotspot_test.go +++ b/routeros/resource_ip_hotspot_test.go @@ -26,7 +26,7 @@ func TestAccIpHotspotTest_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testResourcePrimaryInstanceId(testIpHotspot), resource.TestCheckResourceAttr(testIpHotspot, "name", "server-1"), - resource.TestCheckResourceAttr(testIpHotspot, "interface", "ether2"), + resource.TestCheckResourceAttr(testIpHotspot, "interface", "ether3"), ), }, }, @@ -41,7 +41,7 @@ func testAccIpHotspotConfig() string { resource "routeros_ip_hotspot" "test" { name = "server-1" - interface = "ether2" + interface = "ether3" } `, providerConfig) } diff --git a/routeros/resource_tool_sniffer_test.go b/routeros/resource_tool_sniffer_test.go index 407751a0..54fe627f 100644 --- a/routeros/resource_tool_sniffer_test.go +++ b/routeros/resource_tool_sniffer_test.go @@ -27,7 +27,7 @@ func TestAccToolSnifferTest_basic(t *testing.T) { resource.TestCheckResourceAttr(testToolSniffer, "streaming_enabled", "true"), resource.TestCheckResourceAttr(testToolSniffer, "streaming_server", "192.168.88.5:37008"), resource.TestCheckResourceAttr(testToolSniffer, "filter_stream", "true"), - resource.TestCheckResourceAttr(testToolSniffer, "filter_interface.0", "ether2"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_interface.0", "ether3"), resource.TestCheckResourceAttr(testToolSniffer, "filter_direction", "rx"), resource.TestCheckResourceAttr(testToolSniffer, "filter_operator_between_entries", "or"), ), @@ -39,7 +39,7 @@ func TestAccToolSnifferTest_basic(t *testing.T) { resource.TestCheckResourceAttr(testToolSniffer, "streaming_enabled", "true"), resource.TestCheckResourceAttr(testToolSniffer, "streaming_server", "192.168.88.5:37008"), resource.TestCheckResourceAttr(testToolSniffer, "filter_stream", "true"), - resource.TestCheckResourceAttr(testToolSniffer, "filter_interface.0", "ether2"), + resource.TestCheckResourceAttr(testToolSniffer, "filter_interface.0", "ether3"), resource.TestCheckResourceAttr(testToolSniffer, "filter_direction", "rx"), resource.TestCheckResourceAttr(testToolSniffer, "filter_operator_between_entries", "and"), ), @@ -59,7 +59,7 @@ resource "routeros_tool_sniffer" "test" { streaming_server = "192.168.88.5:37008" filter_stream = true - filter_interface = ["ether2"] + filter_interface = ["ether3"] filter_direction = "rx" filter_operator_between_entries = "%v" }