From c799f299629fd82c3004dd1903487d694aeffbbf Mon Sep 17 00:00:00 2001 From: Vaerh Date: Tue, 1 Oct 2024 09:38:02 +0300 Subject: [PATCH 01/24] feat: Disable warning output on system resources Added disabling of warnings when deleting MT system objects. We can turn them off using `ROS_SUPPRESS_SYSO_DEL_WARN` environment variable or `suppress_syso_del_warn` parameter. --- routeros/mikrotik_client.go | 11 +++++++++++ routeros/mikrotik_client_api.go | 5 +++++ routeros/mikrotik_client_rest.go | 5 +++++ routeros/provider.go | 9 +++++++++ routeros/resource_default_actions.go | 3 +++ 5 files changed, 33 insertions(+) diff --git a/routeros/mikrotik_client.go b/routeros/mikrotik_client.go index 06de33a1..0519e08e 100644 --- a/routeros/mikrotik_client.go +++ b/routeros/mikrotik_client.go @@ -18,6 +18,7 @@ import ( ) type Client interface { + GetExtraParams() *ExtraParams GetTransport() TransportType SendRequest(method crudMethod, url *URL, item MikrotikItem, result interface{}) error } @@ -40,6 +41,10 @@ const ( crudStop ) +type ExtraParams struct { + SuppressSysODelWarn bool +} + func NewClient(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { tlsConf := tls.Config{ @@ -114,6 +119,9 @@ func NewClient(ctx context.Context, d *schema.ResourceData) (interface{}, diag.D Username: d.Get("username").(string), Password: d.Get("password").(string), Transport: TransportAPI, + extra: &ExtraParams{ + SuppressSysODelWarn: d.Get("suppress_syso_del_warn").(bool), + }, } if useTLS { @@ -138,6 +146,9 @@ func NewClient(ctx context.Context, d *schema.ResourceData) (interface{}, diag.D Username: d.Get("username").(string), Password: d.Get("password").(string), Transport: TransportREST, + extra: &ExtraParams{ + SuppressSysODelWarn: d.Get("suppress_syso_del_warn").(bool), + }, } rest.Client = &http.Client{ diff --git a/routeros/mikrotik_client_api.go b/routeros/mikrotik_client_api.go index d4f2e138..0629f5d1 100644 --- a/routeros/mikrotik_client_api.go +++ b/routeros/mikrotik_client_api.go @@ -15,6 +15,7 @@ type ApiClient struct { Username string Password string Transport TransportType + extra *ExtraParams *routeros.Client } @@ -36,6 +37,10 @@ var ( } ) +func (c *ApiClient) GetExtraParams() *ExtraParams { + return c.extra +} + func (c *ApiClient) GetTransport() TransportType { return c.Transport } diff --git a/routeros/mikrotik_client_rest.go b/routeros/mikrotik_client_rest.go index b2afed87..c0f4fa46 100644 --- a/routeros/mikrotik_client_rest.go +++ b/routeros/mikrotik_client_rest.go @@ -16,6 +16,7 @@ type RestClient struct { Username string Password string Transport TransportType + extra *ExtraParams *http.Client } @@ -43,6 +44,10 @@ var ( } ) +func (c *RestClient) GetExtraParams() *ExtraParams { + return c.extra +} + func (c *RestClient) GetTransport() TransportType { return c.Transport } diff --git a/routeros/provider.go b/routeros/provider.go index f5fb3c19..38339ae0 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -76,6 +76,15 @@ func Provider() *schema.Provider { ), Description: "Whether to verify the SSL certificate or not.", }, + "suppress_syso_del_warn": { + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc( + []string{"ROS_SUPPRESS_SYSO_DEL_WARN"}, + false, + ), + Description: "Suppress the system object deletion warning.", + }, }, ResourcesMap: map[string]*schema.Resource{ diff --git a/routeros/resource_default_actions.go b/routeros/resource_default_actions.go index 17ac7868..4fe18887 100644 --- a/routeros/resource_default_actions.go +++ b/routeros/resource_default_actions.go @@ -204,6 +204,9 @@ func SystemResourceCreateUpdate(ctx context.Context, s map[string]*schema.Schema // No delete functionality provided by API for System Resources. func SystemResourceDelete(ctx context.Context, s map[string]*schema.Schema, d *schema.ResourceData, m interface{}) diag.Diagnostics { d.SetId("") + if m.(Client).GetExtraParams().SuppressSysODelWarn { + return nil + } return DeleteSystemObject } From 33793fd70aef7580862b577cd6f001cf9d954306 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 30 Sep 2024 08:32:59 +0300 Subject: [PATCH 02/24] =?UTF-8?q?fix(wireless):=20Delete=20required=20para?= =?UTF-8?q?meters=20Interaction=20with=20physical=20interfaces=20has=20bee?= =?UTF-8?q?n=20simplified.=20It=20is=20possible,=20for=20example,=20to=20e?= =?UTF-8?q?nable/disable=20an=20interface=20without=20specifying=20unneces?= =?UTF-8?q?sary=20parameters.=20resource=20=E2=80=9Crouteros=5Finterface?= =?UTF-8?q?=5Fwireless=E2=80=9D=20=E2=80=98wlan-2ghz=E2=80=99=20{=20=20=20?= =?UTF-8?q?name=20=3D=20=E2=80=9Cwlan1=E2=80=9D=20=20=20disabled=20=3D=20v?= =?UTF-8?q?ar.wlan=5F2ghz=5Fdisabled=20}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routeros/resource_interface_wireless.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/routeros/resource_interface_wireless.go b/routeros/resource_interface_wireless.go index 23085000..e0697f52 100644 --- a/routeros/resource_interface_wireless.go +++ b/routeros/resource_interface_wireless.go @@ -198,6 +198,7 @@ func ResourceInterfaceWireless() *schema.Resource { Description: "Defines set of used data rates, channel frequencies and widths.", ValidateFunc: validation.StringInSlice([]string{"2ghz-b", "2ghz-b/g", "2ghz-b/g/n", "2ghz-onlyg", "2ghz-onlyn", "5ghz-a", "5ghz-a/n", "5ghz-onlyn", "5ghz-a/n/ac", "5ghz-onlyac", "5ghz-n/ac"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, }, "basic_rates_ag": { Type: schema.TypeSet, @@ -483,7 +484,7 @@ func ResourceInterfaceWireless() *schema.Resource { }, "mode": { Type: schema.TypeString, - Required: true, + Optional: true, Description: "Selection between different station and access point (AP) modes. **Station modes**: `station` - Basic " + "station mode. Find and connect to acceptable AP. `station-wds` - Same as station, but create WDS link with " + "AP, using proprietary extension. AP configuration has to allow WDS links with this device. Note that " + @@ -727,9 +728,10 @@ func ResourceInterfaceWireless() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{"integer"}, false), }, "ssid": { - Type: schema.TypeString, - Required: true, - Description: "SSID (service set identifier) is a name that identifies wireless network.", + Type: schema.TypeString, + Optional: true, + Description: "SSID (service set identifier) is a name that identifies wireless network.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, }, "skip_dfs_channels": { Type: schema.TypeString, From cc61117f3ec6ddadebd7fbd4579daa143d2a2bcf Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 30 Sep 2024 09:12:05 +0300 Subject: [PATCH 03/24] docs: Fix `routeros_interface_wireless` example --- .../routeros_wifi_easy_connect/data-source.tf | 3 +++ .../routeros_interface_wireless/resource.tf | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/data-sources/routeros_wifi_easy_connect/data-source.tf b/examples/data-sources/routeros_wifi_easy_connect/data-source.tf index a698a044..e67b8ee1 100644 --- a/examples/data-sources/routeros_wifi_easy_connect/data-source.tf +++ b/examples/data-sources/routeros_wifi_easy_connect/data-source.tf @@ -7,3 +7,6 @@ data "routeros_wifi_easy_connect" "test" { output "qrcode" { value = data.routeros_wifi_easy_connect.test.qr_code } + +# We can disable the QR code output and view it in the state file if needed. +# terraform.exe state show data.routeros_wifi_easy_connect.test diff --git a/examples/resources/routeros_interface_wireless/resource.tf b/examples/resources/routeros_interface_wireless/resource.tf index 6175aedd..575c7546 100644 --- a/examples/resources/routeros_interface_wireless/resource.tf +++ b/examples/resources/routeros_interface_wireless/resource.tf @@ -1,3 +1,13 @@ +variable "wlan_2ghz_disabled" { + type = bool + default = false +} + +resource "routeros_interface_wireless" "wlan-2ghz" { + name = "wlan1" + disabled = var.wlan_2ghz_disabled +} + resource "routeros_interface_wireless_security_profiles" "test" { name = "test-profile" mode = "dynamic-keys" @@ -10,8 +20,8 @@ resource "routeros_interface_wireless" "test" { depends_on = [resource.routeros_interface_wireless_security_profiles.test] security_profile = resource.routeros_interface_wireless_security_profiles.test.name mode = "ap-bridge" - master_interface = "wlan1" + master_interface = resource.routeros_interface_wireless.wlan-2ghz.name name = "wlan-guest" ssid = "guests" basic_rates_ag = ["6Mbps", "9Mbps"] -} \ No newline at end of file +} From bddb8fc39bbcb273ce3701ee2a9db2ad18f6ae0e Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 12:25:11 +0300 Subject: [PATCH 04/24] chore: Modify the boilerplate --- tools/boilerplate/main.go | 49 +++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/tools/boilerplate/main.go b/tools/boilerplate/main.go index deb6722e..40994a9e 100644 --- a/tools/boilerplate/main.go +++ b/tools/boilerplate/main.go @@ -21,8 +21,9 @@ import ( 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.String("table", "", "Extracting attributes from the WIKI table (CSV file)") + isSystem = flag.Bool("system", false, "This is a system resource") + csvTable = flag.String("table", "", "Extracting attributes from the WIKI table (CSV file)") + fromCsvName = flag.Bool("from-csv", false, "Generate resource name from CSV file name routeros_csv_file_name") ) func Fatalf(format string, a ...any) { @@ -63,13 +64,26 @@ func (t ItemType) HCL() string { func main() { flag.Parse() - if len(flag.Args()) < 1 { - Fatalf("Usage: go run tools/bolerplate/main.go [-table file.csv] [-system] ") + if len(flag.Args()) < 1 && !*fromCsvName { + Fatalf(` +Usage: go run tools/bolerplate/main.go [-from-csv] [-table file.csv] [-system] [routeros_new_resource] + go run main.go -from-csv -table ip_ipsec_key.csv + `) } - resName := flag.Args()[0] + var resName string + if len(flag.Args()) > 0 { + resName = flag.Args()[0] + } if !reNewItemName.MatchString(resName) { - Fatalf("The resource name must be in the format: 'routeros_[a-z_]+', got '%v'", resName) + if !*fromCsvName { + Fatalf("The resource name must be in the format: 'routeros_[a-z_]+', got '%v'", resName) + } + + resName = fmt.Sprintf("routeros_%v", strings.TrimSuffix(*csvTable, filepath.Ext(*csvTable))) + if !reNewItemName.MatchString(resName) { + Fatalf("The resource name must be in the format: 'routeros_[a-z_]+', got '%v'", resName) + } } var Schema string @@ -129,7 +143,8 @@ func main() { GoResourceName string ResourceName string ResourcePath string - }{goName, resName, strings.ReplaceAll(strings.TrimPrefix(resName, "routeros_"), "_", "/")}) + System bool + }{goName, resName, strings.ReplaceAll(strings.TrimPrefix(resName, "routeros_"), "_", "/"), *isSystem}) if err != nil { panic(err) } @@ -170,12 +185,12 @@ func main() { } f.Close() - var flags int = os.O_WRONLY | os.O_APPEND - if _, err := os.Stat(filepath.Join("routeros", "provider.go")); err != nil { - flags |= os.O_CREATE - } + // var flags int = os.O_WRONLY | os.O_APPEND + // if _, err := os.Stat(filepath.Join("routeros", "provider.go")); err != nil { + // flags |= os.O_CREATE + // } - f, err = os.OpenFile(filepath.Join("routeros", "provider.go"), flags, 0644) + f, err = os.OpenFile(filepath.Join("routeros", resName+"_provider.go"), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { panic(err) } @@ -217,19 +232,19 @@ func TestAcc{{.GoResourceName}}Test_basic(t *testing.T) { CheckDestroy: testCheckResourceDestroy("/{{.ResourcePath}}", "{{.ResourceName}}"), Steps: []resource.TestStep{ { - Config: testAcc{{.GoResourceName}}Config(""), + Config: testAcc{{.GoResourceName}}Config({{- if .System }}""{{end}}), Check: resource.ComposeTestCheckFunc( testResourcePrimaryInstanceId(test{{.GoResourceName}}), resource.TestCheckResourceAttr(test{{.GoResourceName}}, "", ""), ), - }, + },{{- if .System }} { Config: testAcc{{.GoResourceName}}Config(""), Check: resource.ComposeTestCheckFunc( testResourcePrimaryInstanceId(test{{.GoResourceName}}), resource.TestCheckResourceAttr(test{{.GoResourceName}}, "", ""), ), - }, + },{{end}} }, }) @@ -237,12 +252,12 @@ func TestAcc{{.GoResourceName}}Test_basic(t *testing.T) { } } -func testAcc{{.GoResourceName}}Config(param string) string { +func testAcc{{.GoResourceName}}Config({{- if .System }}param string{{end}}) string { return fmt.Sprintf(` + "`" + `%v resource "{{.ResourceName}}" "test" { } -` + "`" + `, providerConfig, param) +` + "`" + `, providerConfig{{- if .System }}, param{{end}}) } ` From f4ca88a7317156065c211aed568b26c2df34e40f Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 12:32:55 +0300 Subject: [PATCH 05/24] refactor: The MikroTik client * Changed the REST client, all values received from MT are converted to an array of elements. Then one element or all elements are returned. This is due to the return of the response to some actions in the form of an empty array. * A timeout of 59 seconds has been added to the REST client, for signaling through the context before an error is returned from the MT. All MT sessions have a maximum duration of 60 seconds. * Added the ability to specify a CRUD method to the `CreateItem` function and pass it through the context. * Added CRUD option for generating IPSec keys. --- routeros/mikrotik_client.go | 9 +++++-- routeros/mikrotik_client_api.go | 1 + routeros/mikrotik_client_rest.go | 41 ++++++++++++++++++++++++++++++-- routeros/mikrotik_crud.go | 15 ++++++++++-- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/routeros/mikrotik_client.go b/routeros/mikrotik_client.go index 0519e08e..947a8fb5 100644 --- a/routeros/mikrotik_client.go +++ b/routeros/mikrotik_client.go @@ -26,7 +26,8 @@ type Client interface { type crudMethod int const ( - crudCreate crudMethod = iota + crudUnknown crudMethod = iota + crudCreate crudRead crudUpdate crudDelete @@ -39,6 +40,7 @@ const ( crudMove crudStart crudStop + crudGenerateKey ) type ExtraParams struct { @@ -152,7 +154,10 @@ func NewClient(ctx context.Context, d *schema.ResourceData) (interface{}, diag.D } rest.Client = &http.Client{ - Timeout: time.Minute, + // ... By default, CreateContext has a 20 minute timeout ... + // but MT REST API timeout is in 60 seconds for any operation. + // Make the timeout smaller so that the lifetime of the context is less than the lifetime of the session. + Timeout: 59 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tlsConf, }, diff --git a/routeros/mikrotik_client_api.go b/routeros/mikrotik_client_api.go index 0629f5d1..5dba786e 100644 --- a/routeros/mikrotik_client_api.go +++ b/routeros/mikrotik_client_api.go @@ -34,6 +34,7 @@ var ( crudMove: "/move", crudStart: "/start", crudStop: "/stop", + crudGenerateKey: "/generate-key", } ) diff --git a/routeros/mikrotik_client_rest.go b/routeros/mikrotik_client_rest.go index c0f4fa46..8293fed3 100644 --- a/routeros/mikrotik_client_rest.go +++ b/routeros/mikrotik_client_rest.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "reflect" "strings" ) @@ -41,6 +42,7 @@ var ( crudMove: "POST", crudStart: "POST", crudStop: "POST", + crudGenerateKey: "POST", } ) @@ -102,13 +104,24 @@ func (c *RestClient) SendRequest(method crudMethod, url *URL, item MikrotikItem, ColorizedDebug(c.ctx, "response body: "+string(body)) + // Cast single return values to an array. + if !isJsonArray(body) { + body = append([]byte{'['}, append(body, []byte{']'}...)...) + } + + // If the requested value is a slice, then parse the JSON directly into it. + var slice = new([]MikrotikItem) + if isSlice(result) { + slice = result.(*[]MikrotikItem) + } + if len(body) != 0 && result != nil { - if err = json.Unmarshal(body, &result); err != nil { + if err = json.Unmarshal(body, &slice); err != nil { if e, ok := err.(*json.SyntaxError); ok { ColorizedDebug(c.ctx, fmt.Sprintf("json.Unmarshal(response body): syntax error at byte offset %d", e.Offset)) - if err = json.Unmarshal(EscapeChars(body), &result); err != nil { + if err = json.Unmarshal(EscapeChars(body), &slice); err != nil { return fmt.Errorf("json.Unmarshal(response body): %v", err) } } else { @@ -116,5 +129,29 @@ func (c *RestClient) SendRequest(method crudMethod, url *URL, item MikrotikItem, } } } + + if !isSlice(result) && len(*slice) > 0 { + result = &(*slice)[0] + } + return nil } + +// isSlice The function returns information whether the passed parameter is a slice. +// The incoming type is a variable or pointer. +func isSlice(i any) bool { + t := reflect.TypeOf(i) + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + return t.Kind() == reflect.Slice +} + +// isJsonArray The function returns information about the type of JSON response. +// Based on the response, we can cast MT's response to an array of values. +// After some time, we can say that it is easier to operate with an array of values, +// since MT can return '[]' which is not obvious in the process of creating a single resource. +func isJsonArray(b []byte) bool { + b = bytes.TrimLeft(b, " \t\r\n") + return len(b) > 0 && b[0] == '[' +} diff --git a/routeros/mikrotik_crud.go b/routeros/mikrotik_crud.go index 801e7a2b..6eb8a817 100644 --- a/routeros/mikrotik_crud.go +++ b/routeros/mikrotik_crud.go @@ -1,6 +1,7 @@ package routeros import ( + "context" "fmt" ) @@ -14,7 +15,7 @@ var ( // https://help.mikrotik.com/docs/display/ROS/REST+API -func CreateItem(item MikrotikItem, resourcePath string, c Client) (MikrotikItem, error) { +func CreateItem(ctx context.Context, item MikrotikItem, resourcePath string, c Client) (MikrotikItem, error) { if item == nil { return nil, errEmptyItem } @@ -22,8 +23,18 @@ func CreateItem(item MikrotikItem, resourcePath string, c Client) (MikrotikItem, return nil, errEmptyPath } + var crud = crudCreate + + if cm := ctxGetCrudMethod(ctx); cm != crudUnknown { + crud = cm + if c.GetTransport() == TransportREST { + // apiMethodName[crud] is CLI path + resourcePath += apiMethodName[crud] + } + } + res := MikrotikItem{} - err := c.SendRequest(crudCreate, &URL{Path: resourcePath}, item, &res) + err := c.SendRequest(crud, &URL{Path: resourcePath}, item, &res) return res, err } From 9b08eeef2e09c36047a4cf13d147152f40d15643 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 12:37:49 +0300 Subject: [PATCH 06/24] refactor: Add a context --- routeros/resource_routing_table.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routeros/resource_routing_table.go b/routeros/resource_routing_table.go index b2e7c4c0..1c234dba 100644 --- a/routeros/resource_routing_table.go +++ b/routeros/resource_routing_table.go @@ -83,7 +83,7 @@ func ResourceRoutingTable() *schema.Resource { } } - res, err := CreateItem(item, metadata.Path, m.(Client)) + res, err := CreateItem(ctx, item, metadata.Path, m.(Client)) if err != nil { ColorizedDebug(ctx, fmt.Sprintf(ErrorMsgPut, err)) return diag.FromErr(err) From 4e4418eda825703236b26b12de2a0930ff787511 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 12:36:48 +0300 Subject: [PATCH 07/24] refactor: Organizing resource actions * The functions are placed in the appropriate files. * Added `ResourceCreateAndWait` function for creating resources with waiting, it is necessary for correct processing of resources which creation requires long execution time. For example, it is generation of RSA 8192 key. --- ...default_actions.go => resource_actions.go} | 269 ++++++++++-------- routeros/resource_actions_default.go | 103 +++++++ routeros/resource_actions_default_system.go | 46 +++ ...tions_test.go => resource_actions_test.go} | 0 4 files changed, 295 insertions(+), 123 deletions(-) rename routeros/{resource_default_actions.go => resource_actions.go} (57%) create mode 100644 routeros/resource_actions_default.go create mode 100644 routeros/resource_actions_default_system.go rename routeros/{resource_default_actions_test.go => resource_actions_test.go} (100%) diff --git a/routeros/resource_default_actions.go b/routeros/resource_actions.go similarity index 57% rename from routeros/resource_default_actions.go rename to routeros/resource_actions.go index 4fe18887..e3b1dbd5 100644 --- a/routeros/resource_default_actions.go +++ b/routeros/resource_actions.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -33,16 +34,159 @@ func dynamicIdLookup(idType IdType, path string, c Client, d *schema.ResourceDat return (*res)[0].GetID(Id), nil } -// ResourceCreate Creation of a resource in accordance with the TF Schema. +// Passing the called CRUD method on creation through an existing context. +type ctxCrudMethod string + +const ctxCrudMethodKey = "crudMethod" + +// Specifies a CRUD method as part of the resource schema description. +func ctxSetCrudMethod(ctx context.Context, m crudMethod) context.Context { + return context.WithValue(ctx, ctxCrudMethod(ctxCrudMethodKey), m) +} + +// Retrieve a CRUD method as part of the processing of a resource creation request. +func ctxGetCrudMethod(ctx context.Context) crudMethod { + if v := ctx.Value(ctxCrudMethod(ctxCrudMethodKey)); v != nil { + return v.(crudMethod) + } + return crudUnknown +} + +// ResourceCreate - Creation of a resource in accordance with the TF Schema. +// It is possible to transparently pass the request type (CRUD Method) within an existing context. +// +// CreateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +// return ResourceCreate(ctxSetCrudMethod(ctx, crudGenerateKey), resSchema, d, m) +// }, func ResourceCreate(ctx context.Context, s map[string]*schema.Schema, d *schema.ResourceData, m interface{}) diag.Diagnostics { item, metadata := TerraformResourceDataToMikrotik(s, d) - res, err := CreateItem(item, metadata.Path, m.(Client)) + res, err := CreateItem(ctx, item, metadata.Path, m.(Client)) if err != nil { ColorizedDebug(ctx, fmt.Sprintf(ErrorMsgPut, err)) return diag.FromErr(err) } + // Some resources may return an empty array as a response when executing commands other than 'create'. + // For these cases, we will try to find the created element by name (if available). + if res.GetID(Id) == "" && item[KeyName] != "" { + items, err := ReadItems(&ItemId{Name, item[KeyName]}, metadata.Path, m.(Client)) + if err != nil { + return diag.FromErr(err) + } + + if items != nil && len(*items) == 1 { + res = (*items)[0] + } + } + + // ... If no ID is set, Terraform assumes the resource was not created successfully; + // as a result, no state will be saved for that resource. + if res.GetID(Id) == "" { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "The resource ID was not found in the response", + }, + } + } + + // At this time, we have a successfully created resource, + // regardless of the success of its reading. + switch metadata.IdType { + case Id: + // Response ID. + d.SetId(res.GetID(Id)) + case Name: + // Resource ID. + d.SetId(item.GetID(Name)) + } + + // We ask for information again in the case of API. + if m.(Client).GetTransport() == TransportAPI { + r, err := ReadItems(&ItemId{Id, res.GetID(Id)}, metadata.Path, m.(Client)) + if err != nil { + ColorizedDebug(ctx, fmt.Sprintf(ErrorMsgPut, err)) + return diag.FromErr(err) + } + + if len(*r) == 0 { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("Mikrotik resource path='%v' id='%v' not found", + metadata.Path, res.GetID(Id)), + }, + } + } + + res = (*r)[0] + } + + //spew.Dump(res) + return MikrotikResourceDataToTerraform(res, s, d) +} + +func ResourceCreateAndWait(ctx context.Context, s map[string]*schema.Schema, d *schema.ResourceData, m interface{}, timeout time.Duration) diag.Diagnostics { + item, metadata := TerraformResourceDataToMikrotik(s, d) + if item[KeyName] == "" { + panic("Asynchronous resource creation should be applied to objects that have the 'name' attribute.") + } + ColorizedDebug(ctx, fmt.Sprintf("Wait timeout is %s", timeout)) + + // The lifetime of a REST session is 60 seconds. + _, err := CreateItem(ctx, item, metadata.Path, m.(Client)) + if err != nil { + // context deadline exceeded (Client.Timeout exceeded while awaiting headers) + // {"detail":"Session closed","error":400,"message":"Bad Request"} + // from RouterOS device: action timed out - try again, if error continues contact MikroTik support and send a supout file (13) + if !strings.Contains(err.Error(), context.DeadlineExceeded.Error()) && !strings.Contains(err.Error(), "Session closed") && + !strings.Contains(err.Error(), "action timed out - try again") { + ColorizedDebug(ctx, fmt.Sprintf(ErrorMsgPut, err)) + return diag.FromErr(err) + } + ColorizedDebug(ctx, "Timeout, the Create context is canceled, waiting for the resource to be created. "+ + "Session termination by MikroTik is ignored.") + } + + // context deadline exceeded + var res MikrotikItem + + // During RSA key generation, we can get 100% CPU utilization of the MT. + // During this time MT may stop accepting external requests! + localCtx, cancel := context.WithTimeout(context.Background(), timeout) + attempt := 0 + for { + defer cancel() + + // We will try to find the created element by name (if available). + items, err := ReadItems(&ItemId{Name, item[KeyName]}, metadata.Path, m.(Client)) + if err != nil { + ColorizedMessage(ctx, TRACE, fmt.Sprintf("Timeout, the Read context is canceled, waiting for the resource to be created. "+ + "Session termination by MikroTik is ignored. Attempt #%v", attempt)) + attempt++ + } + + if items != nil && len(*items) == 1 { + res = (*items)[0] + break + } + + select { + case <-localCtx.Done(): + // The context deadline has been exceeded. + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "The resource ID was not found in the response", + }, + } + default: + time.Sleep(15 * time.Second) + } + + } + // ... If no ID is set, Terraform assumes the resource was not created successfully; // as a result, no state will be saved for that resource. if res.GetID(Id) == "" { @@ -209,124 +353,3 @@ func SystemResourceDelete(ctx context.Context, s map[string]*schema.Schema, d *s } return DeleteSystemObject } - -func DefaultCreate(s map[string]*schema.Schema) schema.CreateContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - return ResourceCreate(ctx, s, d, m) - } -} - -func DefaultRead(s map[string]*schema.Schema) schema.ReadContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - return ResourceRead(ctx, s, d, m) - } -} - -func DefaultUpdate(s map[string]*schema.Schema) schema.UpdateContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - return ResourceUpdate(ctx, s, d, m) - } -} - -func DefaultDelete(s map[string]*schema.Schema) schema.DeleteContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - return ResourceDelete(ctx, s, d, m) - } -} - -func DefaultValidateCreate(s map[string]*schema.Schema, f DataValidateFunc) schema.CreateContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - if f != nil { - if diags := f(d); diags.HasError() { - return diags - } - } - return ResourceCreate(ctx, s, d, m) - } -} - -func DefaultValidateUpdate(s map[string]*schema.Schema, f DataValidateFunc) schema.UpdateContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - if f != nil { - if diags := f(d); diags.HasError() { - return diags - } - } - return ResourceUpdate(ctx, s, d, m) - } -} - -func DefaultSystemCreate(s map[string]*schema.Schema) schema.CreateContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - return SystemResourceCreateUpdate(ctx, s, d, m) - } -} - -func DefaultSystemRead(s map[string]*schema.Schema) schema.ReadContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - return SystemResourceRead(ctx, s, d, m) - } -} - -func DefaultSystemUpdate(s map[string]*schema.Schema) schema.UpdateContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - return SystemResourceCreateUpdate(ctx, s, d, m) - } -} - -func DefaultSystemDelete(s map[string]*schema.Schema) schema.DeleteContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - return SystemResourceDelete(ctx, s, d, m) - } -} - -func DefaultSystemDatasourceRead(s map[string]*schema.Schema) schema.ReadContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - res := MikrotikItem{} - path := s[MetaResourcePath].Default.(string) - - err := m.(Client).SendRequest(crudRead, &URL{Path: path}, nil, &res) - if err != nil { - return diag.FromErr(err) - } - - 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) - } -} diff --git a/routeros/resource_actions_default.go b/routeros/resource_actions_default.go new file mode 100644 index 00000000..515a5faf --- /dev/null +++ b/routeros/resource_actions_default.go @@ -0,0 +1,103 @@ +package routeros + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func DefaultCreate(s map[string]*schema.Schema) schema.CreateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return ResourceCreate(ctx, s, d, m) + } +} + +func DefaultCreateWithTimeout(s map[string]*schema.Schema, t time.Duration) schema.CreateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return ResourceCreate(ctx, s, d, m) + } +} + +func DefaultValidateCreate(s map[string]*schema.Schema, f DataValidateFunc) schema.CreateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + if f != nil { + if diags := f(d); diags.HasError() { + return diags + } + } + return ResourceCreate(ctx, s, d, m) + } +} + +func DefaultRead(s map[string]*schema.Schema) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return ResourceRead(ctx, s, d, m) + } +} + +func DefaultUpdate(s map[string]*schema.Schema) schema.UpdateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return ResourceUpdate(ctx, s, d, m) + } +} + +func DefaultValidateUpdate(s map[string]*schema.Schema, f DataValidateFunc) schema.UpdateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + if f != nil { + if diags := f(d); diags.HasError() { + return diags + } + } + return ResourceUpdate(ctx, s, d, m) + } +} + +func DefaultDelete(s map[string]*schema.Schema) schema.DeleteContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return ResourceDelete(ctx, s, d, m) + } +} + +// Function to update resources that are present in the system by default out of the box. +// The distinctive feature of such resources is that they cannot be deleted, but they can be modified. +// For example, enabling/disabling the resource. +// +// 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) + } +} diff --git a/routeros/resource_actions_default_system.go b/routeros/resource_actions_default_system.go new file mode 100644 index 00000000..36ae8443 --- /dev/null +++ b/routeros/resource_actions_default_system.go @@ -0,0 +1,46 @@ +package routeros + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func DefaultSystemCreate(s map[string]*schema.Schema) schema.CreateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return SystemResourceCreateUpdate(ctx, s, d, m) + } +} + +func DefaultSystemRead(s map[string]*schema.Schema) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return SystemResourceRead(ctx, s, d, m) + } +} + +func DefaultSystemUpdate(s map[string]*schema.Schema) schema.UpdateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return SystemResourceCreateUpdate(ctx, s, d, m) + } +} + +func DefaultSystemDelete(s map[string]*schema.Schema) schema.DeleteContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return SystemResourceDelete(ctx, s, d, m) + } +} + +func DefaultSystemDatasourceRead(s map[string]*schema.Schema) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + res := MikrotikItem{} + path := s[MetaResourcePath].Default.(string) + + err := m.(Client).SendRequest(crudRead, &URL{Path: path}, nil, &res) + if err != nil { + return diag.FromErr(err) + } + + return MikrotikResourceDataToTerraformDatasource(&[]MikrotikItem{res}, "", s, d) + } +} diff --git a/routeros/resource_default_actions_test.go b/routeros/resource_actions_test.go similarity index 100% rename from routeros/resource_default_actions_test.go rename to routeros/resource_actions_test.go From c87e06d6f78518f5130a8bf619dd15105aba2d70 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 14:38:03 +0300 Subject: [PATCH 08/24] refactor: Move the `ImportStateCustomContext` function --- routeros/provider_schema_helpers.go | 49 ----------------------------- routeros/resource_actions.go | 48 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/routeros/provider_schema_helpers.go b/routeros/provider_schema_helpers.go index e891a23c..68b15b93 100644 --- a/routeros/provider_schema_helpers.go +++ b/routeros/provider_schema_helpers.go @@ -1,7 +1,6 @@ package routeros import ( - "context" "fmt" "regexp" "strconv" @@ -763,51 +762,3 @@ var DeleteSystemObject = []diag.Diagnostic{{ "This action will remove the object from the Terraform state. " + "See also: 'terraform state rm' https://developer.hashicorp.com/terraform/cli/commands/state/rm", }} - -// ImportStateCustomContext is an implementation of StateContextFunc that can be used to -// import resources with the ability to explicitly or implicitly specify a key field. -// `terraform [global options] import [options] ADDR ID`. -// During import the content of the `ID` is checked and depending on the specified string it is possible to automatically search for the internal Mikrotik identifier. -// Logic of `ID` processing -// - The first character of the string contains an asterisk (standard Mikrotik identifier `*3E`): import without additional search. -// - String containing no "=" character (`wifi-01`): the "name" field is used for searching. -// - String containing only one "=" character (`"comment=hAP-ac3"`): the "word left" and "word right" pair is used for searching. -func ImportStateCustomContext(s map[string]*schema.Schema) schema.StateContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { - id := d.Id() - fieldName := "name" - - if len(id) == 0 || id[0] == '*' { - return []*schema.ResourceData{d}, nil - } else { - // By default, we filter by the "name" field - if s := strings.Split(id, "="); len(s) == 2 { - // field=value - fieldName = s[0] - id = s[1] - } - } - - path := s[MetaResourcePath].Default.(string) - - res, err := ReadItemsFiltered([]string{SnakeToKebab(fieldName) + "=" + id}, path, m.(Client)) - if err != nil { - return nil, err - } - - switch len(*res) { - case 0: - return nil, fmt.Errorf("resource not found: %v=%v", fieldName, id) - case 1: - retId, ok := (*res)[0][Id.String()] - if !ok { - return nil, fmt.Errorf("attribute %v not found in the response", Id.String()) - } - d.SetId(retId) - default: - return nil, fmt.Errorf("more than one resource found: %v=%v", fieldName, id) - } - - return []*schema.ResourceData{d}, nil - } -} diff --git a/routeros/resource_actions.go b/routeros/resource_actions.go index e3b1dbd5..96c2b0a7 100644 --- a/routeros/resource_actions.go +++ b/routeros/resource_actions.go @@ -353,3 +353,51 @@ func SystemResourceDelete(ctx context.Context, s map[string]*schema.Schema, d *s } return DeleteSystemObject } + +// ImportStateCustomContext is an implementation of StateContextFunc that can be used to +// import resources with the ability to explicitly or implicitly specify a key field. +// `terraform [global options] import [options] ADDR ID`. +// During import the content of the `ID` is checked and depending on the specified string it is possible to automatically search for the internal Mikrotik identifier. +// Logic of `ID` processing +// - The first character of the string contains an asterisk (standard Mikrotik identifier `*3E`): import without additional search. +// - String containing no "=" character (`wifi-01`): the "name" field is used for searching. +// - String containing only one "=" character (`"comment=hAP-ac3"`): the "word left" and "word right" pair is used for searching. +func ImportStateCustomContext(s map[string]*schema.Schema) schema.StateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + id := d.Id() + fieldName := "name" + + if len(id) == 0 || id[0] == '*' { + return []*schema.ResourceData{d}, nil + } else { + // By default, we filter by the "name" field + if s := strings.Split(id, "="); len(s) == 2 { + // field=value + fieldName = s[0] + id = s[1] + } + } + + path := s[MetaResourcePath].Default.(string) + + res, err := ReadItemsFiltered([]string{SnakeToKebab(fieldName) + "=" + id}, path, m.(Client)) + if err != nil { + return nil, err + } + + switch len(*res) { + case 0: + return nil, fmt.Errorf("resource not found: %v=%v", fieldName, id) + case 1: + retId, ok := (*res)[0][Id.String()] + if !ok { + return nil, fmt.Errorf("attribute %v not found in the response", Id.String()) + } + d.SetId(retId) + default: + return nil, fmt.Errorf("more than one resource found: %v=%v", fieldName, id) + } + + return []*schema.ResourceData{d}, nil + } +} From 14865b9fa989e79a07a9e46857399cf560208f61 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 14:48:07 +0300 Subject: [PATCH 09/24] feat(ipsec): Add new resource `routeros_ip_ipsec_key` --- .../resources/routeros_ip_ipsec_key/import.sh | 5 ++ .../routeros_ip_ipsec_key/resource.tf | 4 ++ routeros/provider.go | 3 +- routeros/resource_ip_ipsec_key.go | 53 +++++++++++++++++++ routeros/resource_ip_ipsec_key_test.go | 46 ++++++++++++++++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 examples/resources/routeros_ip_ipsec_key/import.sh create mode 100644 examples/resources/routeros_ip_ipsec_key/resource.tf create mode 100644 routeros/resource_ip_ipsec_key.go create mode 100644 routeros/resource_ip_ipsec_key_test.go diff --git a/examples/resources/routeros_ip_ipsec_key/import.sh b/examples/resources/routeros_ip_ipsec_key/import.sh new file mode 100644 index 00000000..41c4ef61 --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_key/import.sh @@ -0,0 +1,5 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/ipsec/key get [print show-ids]] +terraform import routeros_ip_ipsec_key.test *3 +#Or you can import a certificate using one of its attributes +terraform import routeros_ip_ipsec_key.test "name=test-key" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_key/resource.tf b/examples/resources/routeros_ip_ipsec_key/resource.tf new file mode 100644 index 00000000..00e27c7c --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_key/resource.tf @@ -0,0 +1,4 @@ +resource "routeros_ip_ipsec_key" "test" { + name = "test-key" + key_size = 2048 +} diff --git a/routeros/provider.go b/routeros/provider.go index 38339ae0..9016c48e 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -258,7 +258,8 @@ func Provider() *schema.Provider { "routeros_routing_ospf_interface_template": ResourceRoutingOspfInterfaceTemplate(), // VPN - "routeros_ovpn_server": ResourceOpenVPNServer(), + "routeros_ip_ipsec_key": ResourceIpIpsecKey(), + "routeros_ovpn_server": ResourceOpenVPNServer(), // PPP "routeros_ppp_aaa": ResourcePppAaa(), diff --git a/routeros/resource_ip_ipsec_key.go b/routeros/resource_ip_ipsec_key.go new file mode 100644 index 00000000..c99241ae --- /dev/null +++ b/routeros/resource_ip_ipsec_key.go @@ -0,0 +1,53 @@ +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" +) + +/* + { + ".id": "*1", + "key-size": "1024", <<<< !!! /ip/ipsec/key/generate-key name=new-key key-size= 2048 4096 8192 + "name": "new-key", + "private-key": "true", + "rsa": "true" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/IPsec#IPsec-Keys +func ResourceIpIpsecKey() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/ipsec/key"), + MetaId: PropId(Id), + MetaSkipFields: PropSkipFields("private_key", "rsa"), + + "key_size": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "Size of this key.", + ValidateFunc: validation.IntInSlice([]int{1024, 2048, 4096}), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + KeyName: PropName(""), + } + + return &schema.Resource{ + CreateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return ResourceCreateAndWait(ctxSetCrudMethod(ctx, crudGenerateKey), resSchema, d, m, d.Timeout(schema.TimeoutCreate)) + }, + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: ImportStateCustomContext(resSchema), + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_ipsec_key_test.go b/routeros/resource_ip_ipsec_key_test.go new file mode 100644 index 00000000..207c3c11 --- /dev/null +++ b/routeros/resource_ip_ipsec_key_test.go @@ -0,0 +1,46 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpIpsecKey = "routeros_ip_ipsec_key.test" + +func TestAccIpIpsecKeyTest_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/ipsec/key", "routeros_ip_ipsec_key"), + Steps: []resource.TestStep{ + { + Config: testAccIpIpsecKeyConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecKey), + resource.TestCheckResourceAttr(testIpIpsecKey, "name", "test-key"), + resource.TestCheckResourceAttr(testIpIpsecKey, "key_size", "2048"), + ), + }, + }, + }) + }) + } +} + +func testAccIpIpsecKeyConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_ipsec_key" "test" { + name = "test-key" + key_size = 2048 +} +`, providerConfig) +} From f2ec9dc9bd2663fb9aeceeb9569051244f5da6cd Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 14:48:34 +0300 Subject: [PATCH 10/24] chore: Modify the boilerplate --- tools/boilerplate/main.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/boilerplate/main.go b/tools/boilerplate/main.go index 40994a9e..2bf4b80e 100644 --- a/tools/boilerplate/main.go +++ b/tools/boilerplate/main.go @@ -201,7 +201,9 @@ Usage: go run tools/bolerplate/main.go [-from-csv] [-table file.csv] [-system] var exampleImportFile = `#The ID can be found via API or the terminal #The command for the terminal is -> :put [/{{.ResourcePath}} get [print show-ids]] -terraform import {{.ResourceName}}.test *3` +terraform import {{.ResourceName}}.test *3 +#Or you can import a certificate using one of its attributes +terraform import {{.ResourceName}}.test "name=xxx"` var exampleResourceFile = ` resource "{{.ResourceName}}" "test" { @@ -289,6 +291,7 @@ func {{.GoResourceName}}() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportStateCustomContext(resSchema), }, Schema: resSchema, From ca88a77f14bd5177938d20930da4ccd515e647fa Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 15:27:49 +0300 Subject: [PATCH 11/24] feat(ipsec): Add new resource `routeros_ip_ipsec_mode_config` --- .../routeros_ip_ipsec_mode_config/import.sh | 5 + .../routeros_ip_ipsec_mode_config/resource.tf | 6 + routeros/provider.go | 5 +- routeros/resource_ip_ipsec_mode_config.go | 107 ++++++++++++++++++ .../resource_ip_ipsec_mode_config_test.go | 51 +++++++++ 5 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 examples/resources/routeros_ip_ipsec_mode_config/import.sh create mode 100644 examples/resources/routeros_ip_ipsec_mode_config/resource.tf create mode 100644 routeros/resource_ip_ipsec_mode_config.go create mode 100644 routeros/resource_ip_ipsec_mode_config_test.go diff --git a/examples/resources/routeros_ip_ipsec_mode_config/import.sh b/examples/resources/routeros_ip_ipsec_mode_config/import.sh new file mode 100644 index 00000000..84913c2c --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_mode_config/import.sh @@ -0,0 +1,5 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/ipsec/mode/config get [print show-ids]] +terraform import routeros_ip_ipsec_mode_config.test *3 +#Or you can import a certificate using one of its attributes +terraform import routeros_ip_ipsec_mode_config.test "address=1.2.3.4" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_mode_config/resource.tf b/examples/resources/routeros_ip_ipsec_mode_config/resource.tf new file mode 100644 index 00000000..e0d79fde --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_mode_config/resource.tf @@ -0,0 +1,6 @@ +resource "routeros_ip_ipsec_mode_config" "test" { + name = "test-cfg" + address = "1.2.3.4" + split_include = ["0.0.0.0/0"] + split_dns = ["1.1.1.1"] +} diff --git a/routeros/provider.go b/routeros/provider.go index 9016c48e..6f7f116c 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -258,8 +258,9 @@ func Provider() *schema.Provider { "routeros_routing_ospf_interface_template": ResourceRoutingOspfInterfaceTemplate(), // VPN - "routeros_ip_ipsec_key": ResourceIpIpsecKey(), - "routeros_ovpn_server": ResourceOpenVPNServer(), + "routeros_ip_ipsec_key": ResourceIpIpsecKey(), + "routeros_ip_ipsec_mode_config": ResourceIpIpsecModeConfig(), + "routeros_ovpn_server": ResourceOpenVPNServer(), // PPP "routeros_ppp_aaa": ResourcePppAaa(), diff --git a/routeros/resource_ip_ipsec_mode_config.go b/routeros/resource_ip_ipsec_mode_config.go new file mode 100644 index 00000000..d8434a27 --- /dev/null +++ b/routeros/resource_ip_ipsec_mode_config.go @@ -0,0 +1,107 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*3", + "address-pool": "default-dhcp", + "address-prefix-length": "24", + "name": "cfg1", + "responder": "true", + "split-dns": "1.1.1.1", + "split-include": "0.0.0.0/0", + "system-dns": "true" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/IPsec#IPsec-Modeconfigs +func ResourceIpIpsecModeConfig() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/ipsec/mode-config"), + MetaId: PropId(Id), + + "address": { + Type: schema.TypeString, + Optional: true, + Description: "Single IP address for the initiator instead of specifying a whole address pool.", + ValidateFunc: validation.IsIPv4Address, + ConflictsWith: []string{"address_pool"}, + }, + "address_pool": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the address pool from which the responder will try to assign address if mode-config " + + "is enabled.", + ConflictsWith: []string{"address"}, + }, + "address_prefix_length": { + Type: schema.TypeInt, + Optional: true, + Description: "Prefix length (netmask) of the assigned address from the pool.", + ValidateFunc: validation.IntBetween(1, 32), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + // KeyComment: PropCommentRw, + KeyName: PropName(""), + "responder": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies whether the configuration will work as an initiator (client) or responder (server). " + + "The initiator will request for mode-config parameters from the responder.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "split_dns": { + Type: schema.TypeSet, + Optional: true, + Description: "List of DNS names that will be resolved using a system-dns=yes or static-dns= setting.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsIPv4Address, + }, + }, + "split_include": { + Type: schema.TypeSet, + Optional: true, + Description: "List of subnets in CIDR format, which to tunnel. Subnets will be sent to the peer using the " + + "CISCO UNITY extension, a remote peer will create specific dynamic policies.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsCIDR, + }, + }, + "src_address_list": { + Type: schema.TypeString, + Optional: true, + Description: "Specifying an address list will generate dynamic source NAT rules. This parameter is only " + + "available with responder=no. A roadWarrior client with NAT.", + }, + "static_dns": { + Type: schema.TypeString, + Optional: true, + Description: "Manually specified DNS server's IP address to be sent to the client.", + }, + "system_dns": { + Type: schema.TypeBool, + Optional: true, + Description: "When this option is enabled DNS addresses will be taken from `/ip dns`.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: ImportStateCustomContext(resSchema), + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_ipsec_mode_config_test.go b/routeros/resource_ip_ipsec_mode_config_test.go new file mode 100644 index 00000000..18c9a176 --- /dev/null +++ b/routeros/resource_ip_ipsec_mode_config_test.go @@ -0,0 +1,51 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpIpsecModeConfig = "routeros_ip_ipsec_mode_config.test" + +func TestAccIpIpsecModeConfigTest_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/ipsec/mode-config", "routeros_ip_ipsec_mode_config"), + Steps: []resource.TestStep{ + { + Config: testAccIpIpsecModeConfigConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecModeConfig), + resource.TestCheckResourceAttr(testIpIpsecModeConfig, "name", "test-cfg"), + resource.TestCheckResourceAttr(testIpIpsecModeConfig, "address", "1.2.3.4"), + resource.TestCheckResourceAttr(testIpIpsecModeConfig, "split_include.0", "0.0.0.0/0"), + resource.TestCheckResourceAttr(testIpIpsecModeConfig, "split_dns.0", "1.1.1.1"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpIpsecModeConfigConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_ipsec_mode_config" "test" { + name = "test-cfg" + address = "1.2.3.4" + split_include = ["0.0.0.0/0"] + split_dns = ["1.1.1.1"] +} +`, providerConfig) +} From d4c0817f6af31a3b5d1fa766bf9c4c415b43aefd Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 15:44:41 +0300 Subject: [PATCH 12/24] feat(ipsec): Add new resource `routeros_ip_ipsec_policy_group` --- .../routeros_ip_ipsec_policy_group/import.sh | 5 ++ .../resource.tf | 3 + routeros/provider.go | 7 ++- routeros/resource_ip_ipsec_policy_group.go | 38 ++++++++++++ .../resource_ip_ipsec_policy_group_test.go | 58 +++++++++++++++++++ 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 examples/resources/routeros_ip_ipsec_policy_group/import.sh create mode 100644 examples/resources/routeros_ip_ipsec_policy_group/resource.tf create mode 100644 routeros/resource_ip_ipsec_policy_group.go create mode 100644 routeros/resource_ip_ipsec_policy_group_test.go diff --git a/examples/resources/routeros_ip_ipsec_policy_group/import.sh b/examples/resources/routeros_ip_ipsec_policy_group/import.sh new file mode 100644 index 00000000..da29b4aa --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_policy_group/import.sh @@ -0,0 +1,5 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/ipsec/policy/group get [print show-ids]] +terraform import routeros_ip_ipsec_policy_group.test *3 +#Or you can import a certificate using one of its attributes +terraform import routeros_ip_ipsec_policy_group.test "name=test-group" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_policy_group/resource.tf b/examples/resources/routeros_ip_ipsec_policy_group/resource.tf new file mode 100644 index 00000000..a93364f5 --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_policy_group/resource.tf @@ -0,0 +1,3 @@ +resource "routeros_ip_ipsec_policy_group" "test" { + name = "test-group" +} \ No newline at end of file diff --git a/routeros/provider.go b/routeros/provider.go index 6f7f116c..f7f6d02e 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -258,9 +258,10 @@ func Provider() *schema.Provider { "routeros_routing_ospf_interface_template": ResourceRoutingOspfInterfaceTemplate(), // VPN - "routeros_ip_ipsec_key": ResourceIpIpsecKey(), - "routeros_ip_ipsec_mode_config": ResourceIpIpsecModeConfig(), - "routeros_ovpn_server": ResourceOpenVPNServer(), + "routeros_ip_ipsec_key": ResourceIpIpsecKey(), + "routeros_ip_ipsec_mode_config": ResourceIpIpsecModeConfig(), + "routeros_ip_ipsec_policy_group": ResourceIpIpsecPolicyGroup(), + "routeros_ovpn_server": ResourceOpenVPNServer(), // PPP "routeros_ppp_aaa": ResourcePppAaa(), diff --git a/routeros/resource_ip_ipsec_policy_group.go b/routeros/resource_ip_ipsec_policy_group.go new file mode 100644 index 00000000..425e2988 --- /dev/null +++ b/routeros/resource_ip_ipsec_policy_group.go @@ -0,0 +1,38 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +/* + { + ".id": "*2", + "default": "true", + "name": "default" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/IPsec#IPsec-Groups +func ResourceIpIpsecPolicyGroup() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/ipsec/policy/group"), + MetaId: PropId(Id), + + KeyDefault: PropDefaultRo, + KeyName: PropName(""), + KeyComment: PropCommentRw, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: ImportStateCustomContext(resSchema), + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_ipsec_policy_group_test.go b/routeros/resource_ip_ipsec_policy_group_test.go new file mode 100644 index 00000000..65506453 --- /dev/null +++ b/routeros/resource_ip_ipsec_policy_group_test.go @@ -0,0 +1,58 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +const testIpIpsecPolicyGroup = "routeros_ip_ipsec_policy_group.test" + +func TestAccIpIpsecPolicyGroupTest_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/ipsec/policy/group", "routeros_ip_ipsec_policy_group"), + Steps: []resource.TestStep{ + { + Config: testAccIpIpsecPolicyGroupConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecPolicyGroup), + resource.TestCheckResourceAttr(testIpIpsecPolicyGroup, "name", "test-group"), + ), + }, + { + Config: testAccIpIpsecPolicyGroupConfig(), + ResourceName: testIpIpsecPolicyGroup, + ImportStateId: `name=test-group`, + ImportState: true, + ImportStateCheck: func(states []*terraform.InstanceState) error { + if len(states) != 1 { + return fmt.Errorf("more than 1 states received, only one expected") + } + return nil + }, + }, + }, + }) + + }) + } +} + +func testAccIpIpsecPolicyGroupConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_ipsec_policy_group" "test" { + name = "test-group" +} +`, providerConfig) +} From 66aa2f8c830a4cfe925a315c36114ded9842264d Mon Sep 17 00:00:00 2001 From: Vaerh Date: Fri, 4 Oct 2024 16:13:30 +0300 Subject: [PATCH 13/24] feat(ipsec): Add new resource `routeros_ip_ipsec_profile` --- .../routeros_ip_ipsec_profile/import.sh | 5 + .../routeros_ip_ipsec_profile/resource.tf | 7 + routeros/provider.go | 1 + routeros/resource_ip_ipsec_profile.go | 126 ++++++++++++++++++ routeros/resource_ip_ipsec_profile_test.go | 67 ++++++++++ 5 files changed, 206 insertions(+) create mode 100644 examples/resources/routeros_ip_ipsec_profile/import.sh create mode 100644 examples/resources/routeros_ip_ipsec_profile/resource.tf create mode 100644 routeros/resource_ip_ipsec_profile.go create mode 100644 routeros/resource_ip_ipsec_profile_test.go diff --git a/examples/resources/routeros_ip_ipsec_profile/import.sh b/examples/resources/routeros_ip_ipsec_profile/import.sh new file mode 100644 index 00000000..c07d41e2 --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_profile/import.sh @@ -0,0 +1,5 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/ipsec/profile get [print show-ids]] +terraform import routeros_ip_ipsec_profile.test *3 +#Or you can import a certificate using one of its attributes +terraform import routeros_ip_ipsec_profile.test "name=test-profile" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_profile/resource.tf b/examples/resources/routeros_ip_ipsec_profile/resource.tf new file mode 100644 index 00000000..f3fc1c98 --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_profile/resource.tf @@ -0,0 +1,7 @@ +resource "routeros_ip_ipsec_profile" "test" { + name = "test-profile" + hash_algorithm = "sha256" + enc_algorithm = ["aes-192", "aes-256"] + dh_group = ["ecp384", "ecp521"] + nat_traversal = false +} \ No newline at end of file diff --git a/routeros/provider.go b/routeros/provider.go index f7f6d02e..3d805e03 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -261,6 +261,7 @@ func Provider() *schema.Provider { "routeros_ip_ipsec_key": ResourceIpIpsecKey(), "routeros_ip_ipsec_mode_config": ResourceIpIpsecModeConfig(), "routeros_ip_ipsec_policy_group": ResourceIpIpsecPolicyGroup(), + "routeros_ip_ipsec_profile": ResourceIpIpsecProfile(), "routeros_ovpn_server": ResourceOpenVPNServer(), // PPP diff --git a/routeros/resource_ip_ipsec_profile.go b/routeros/resource_ip_ipsec_profile.go new file mode 100644 index 00000000..910665a0 --- /dev/null +++ b/routeros/resource_ip_ipsec_profile.go @@ -0,0 +1,126 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*A", + "default": "true", + "dh-group": "modp2048,modp1024", + "dpd-interval": "2m", + "dpd-maximum-failures": "5", + "enc-algorithm": "aes-128,3des", + "hash-algorithm": "sha1", + "lifetime": "1d", + "name": "default", + "nat-traversal": "true", + "proposal-check": "obey" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/IPsec#IPsec-Profiles +func ResourceIpIpsecProfile() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/ipsec/profile"), + MetaId: PropId(Id), + + "dh_group": { + Type: schema.TypeSet, + Optional: true, + Description: "Diffie-Hellman group (cipher strength).", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"modp768", "modp1024 ", "modp1536", "modp2048", + "modp3072", "modp4096", "modp6144", "modp8192", "ecp256", "ecp384", "ecp521"}, false), + }, + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "dpd_interval": { + Type: schema.TypeString, + Optional: true, + Description: "Dead peer detection interval. If set to disable-dpd, dead peer detection will not be used.", + DiffSuppressFunc: TimeEquall, + }, + "dpd_maximum_failures": { + Type: schema.TypeInt, + Optional: true, + Description: "Maximum count of failures until peer is considered to be dead. Applicable if DPD is enabled.", + ValidateFunc: validation.IntBetween(1, 100), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "enc_algorithm": { + Type: schema.TypeSet, + Optional: true, + Description: "List of encryption algorithms that will be used by the peer.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"3des", "aes-128", "aes-192", "aes-256", "blowfish", + "camellia-128", "camellia-192", "camellia-256", "des"}, false), + }, + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "hash_algorithm": { + Type: schema.TypeString, + Optional: true, + Description: "Hashing algorithm. SHA (Secure Hash Algorithm) is stronger, but slower. MD5 uses 128-bit key, " + + "sha1-160bit key.", + ValidateFunc: validation.StringInSlice([]string{"md5", "sha1", "sha256", "sha512"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "lifebytes": { + Type: schema.TypeInt, + Optional: true, + Description: "Phase 1 lifebytes is used only as administrative value which is added to proposal. Used in " + + "cases if remote peer requires specific lifebytes value to establish phase 1.", + }, + "lifetime": { + Type: schema.TypeString, + Optional: true, + Description: "Phase 1 lifetime: specifies how long the SA will be valid.", + DiffSuppressFunc: TimeEquall, + }, + KeyName: PropName(""), + "nat_traversal": { + Type: schema.TypeBool, + Optional: true, + Description: "Use Linux NAT-T mechanism to solve IPsec incompatibility with NAT routers between IPsec peers. " + + "This can only be used with ESP protocol (AH is not supported by design, as it signs the complete packet, " + + "including the IP header, which is changed by NAT, rendering AH signature invalid). The method encapsulates " + + "IPsec ESP traffic into UDP streams in order to overcome some minor issues that made ESP incompatible " + + "with NAT.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "prf_algorithm": { + Type: schema.TypeString, + Optional: true, + Description: "", + ValidateFunc: validation.StringInSlice([]string{"auto", "sha1", "sha256", "sha384", "sha512"}, false), + }, + "proposal_check": { + Type: schema.TypeString, + Optional: true, + Description: "Phase 2 lifetime check logic:claim - take shortest of proposed and configured lifetimes and " + + "notify initiator about itexact - require lifetimes to be the sameobey - accept whatever is sent by an " + + "initiatorstrict - if the proposed lifetime is longer than the default then reject the proposal otherwise " + + "accept a proposed lifetime.", + ValidateFunc: validation.StringInSlice([]string{"claim", "exact", "obey", "strict"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: ImportStateCustomContext(resSchema), + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_ipsec_profile_test.go b/routeros/resource_ip_ipsec_profile_test.go new file mode 100644 index 00000000..5286e649 --- /dev/null +++ b/routeros/resource_ip_ipsec_profile_test.go @@ -0,0 +1,67 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +const testIpIpsecProfile = "routeros_ip_ipsec_profile.test" + +func TestAccIpIpsecProfileTest_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/ipsec/profile", "routeros_ip_ipsec_profile"), + Steps: []resource.TestStep{ + { + Config: testAccIpIpsecProfileConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecProfile), + resource.TestCheckResourceAttr(testIpIpsecProfile, "name", "test-profile"), + resource.TestCheckResourceAttr(testIpIpsecProfile, "hash_algorithm", "sha256"), + resource.TestCheckResourceAttr(testIpIpsecProfile, "enc_algorithm.#", "2"), + resource.TestCheckResourceAttr(testIpIpsecProfile, "enc_algorithm.0", "aes-192"), + resource.TestCheckResourceAttr(testIpIpsecProfile, "enc_algorithm.1", "aes-256"), + resource.TestCheckResourceAttr(testIpIpsecProfile, "nat_traversal", "false"), + ), + }, + { + Config: testAccIpIpsecProfileConfig(), + ResourceName: testIpIpsecProfile, + ImportStateId: `name=test-profile`, + ImportState: true, + ImportStateCheck: func(states []*terraform.InstanceState) error { + if len(states) != 1 { + return fmt.Errorf("more than 1 states received, only one expected") + } + return nil + }, + }, + }, + }) + + }) + } +} + +func testAccIpIpsecProfileConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_ipsec_profile" "test" { + name = "test-profile" + hash_algorithm = "sha256" + enc_algorithm = ["aes-192", "aes-256"] + dh_group = ["ecp384", "ecp521"] + nat_traversal = false +} +`, providerConfig) +} From da7ea5d5994190d9e15e8c969cabc578a4f1b64d Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 10:33:31 +0300 Subject: [PATCH 14/24] refactor: Fix serialization. System resource request contains null pointer to `MikrotikItem`. --- routeros/mikrotik_client_rest.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routeros/mikrotik_client_rest.go b/routeros/mikrotik_client_rest.go index 8293fed3..ca5406f5 100644 --- a/routeros/mikrotik_client_rest.go +++ b/routeros/mikrotik_client_rest.go @@ -111,7 +111,7 @@ func (c *RestClient) SendRequest(method crudMethod, url *URL, item MikrotikItem, // If the requested value is a slice, then parse the JSON directly into it. var slice = new([]MikrotikItem) - if isSlice(result) { + if result != nil && isSlice(result) { slice = result.(*[]MikrotikItem) } @@ -130,7 +130,7 @@ func (c *RestClient) SendRequest(method crudMethod, url *URL, item MikrotikItem, } } - if !isSlice(result) && len(*slice) > 0 { + if result != nil && !isSlice(result) && len(*slice) > 0 { result = &(*slice)[0] } From 7388cae3a5753ef9c0dfbf5c2423497ee9eba838 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 10:36:44 +0300 Subject: [PATCH 15/24] feat(ipsec): Add new resource `routeros_ip_ipsec_settings` --- .../routeros_ip_ipsec_settings/import.sh | 1 + .../routeros_ip_ipsec_settings/resource.tf | 4 ++ routeros/provider.go | 1 + routeros/resource_ip_ipsec_settings.go | 53 ++++++++++++++++++ routeros/resource_ip_ipsec_settings_test.go | 54 +++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 examples/resources/routeros_ip_ipsec_settings/import.sh create mode 100644 examples/resources/routeros_ip_ipsec_settings/resource.tf create mode 100644 routeros/resource_ip_ipsec_settings.go create mode 100644 routeros/resource_ip_ipsec_settings_test.go diff --git a/examples/resources/routeros_ip_ipsec_settings/import.sh b/examples/resources/routeros_ip_ipsec_settings/import.sh new file mode 100644 index 00000000..e4252eb3 --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_settings/import.sh @@ -0,0 +1 @@ +terraform import routeros_ip_ipsec_settings.test . \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_settings/resource.tf b/examples/resources/routeros_ip_ipsec_settings/resource.tf new file mode 100644 index 00000000..d08663a2 --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_settings/resource.tf @@ -0,0 +1,4 @@ +resource "routeros_ip_ipsec_settings" "test" { + xauth_use_radius = true + interim_update = "60s" +} \ No newline at end of file diff --git a/routeros/provider.go b/routeros/provider.go index 3d805e03..f53beb23 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -262,6 +262,7 @@ func Provider() *schema.Provider { "routeros_ip_ipsec_mode_config": ResourceIpIpsecModeConfig(), "routeros_ip_ipsec_policy_group": ResourceIpIpsecPolicyGroup(), "routeros_ip_ipsec_profile": ResourceIpIpsecProfile(), + "routeros_ip_ipsec_settings": ResourceIpIpsecSettings(), "routeros_ovpn_server": ResourceOpenVPNServer(), // PPP diff --git a/routeros/resource_ip_ipsec_settings.go b/routeros/resource_ip_ipsec_settings.go new file mode 100644 index 00000000..624c8231 --- /dev/null +++ b/routeros/resource_ip_ipsec_settings.go @@ -0,0 +1,53 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +/* +REST JSON +*/ + +// https://help.mikrotik.com/docs/display/ROS/IPsec#IPsec-Settings +func ResourceIpIpsecSettings() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/ipsec/settings"), + MetaId: PropId(Id), + + "accounting": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to send RADIUS accounting requests to a RADIUS server. Applicable if EAP Radius " + + "(`auth-method=eap-radius`) or pre-shared key with XAuth authentication method " + + "(`auth-method=pre-shared-key-xauth`) is used.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "interim_update": { + Type: schema.TypeString, + Optional: true, + Description: "The interval between each consecutive RADIUS accounting Interim update. Accounting must be " + + "enabled.", + DiffSuppressFunc: TimeEquall, + }, + "xauth_use_radius": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to use Radius client for XAuth users or not. Property is only applicable to peers " + + "using the IKEv1 exchange mode.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + } + + return &schema.Resource{ + CreateContext: DefaultSystemCreate(resSchema), + ReadContext: DefaultSystemRead(resSchema), + UpdateContext: DefaultSystemUpdate(resSchema), + DeleteContext: DefaultSystemDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_ipsec_settings_test.go b/routeros/resource_ip_ipsec_settings_test.go new file mode 100644 index 00000000..979e1ee5 --- /dev/null +++ b/routeros/resource_ip_ipsec_settings_test.go @@ -0,0 +1,54 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpIpsecSettings = "routeros_ip_ipsec_settings.test" + +func TestAccIpIpsecSettingsTest_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: testAccIpIpsecSettingsConfig("true", "10s"), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecSettings), + resource.TestCheckResourceAttr(testIpIpsecSettings, "xauth_use_radius", "true"), + resource.TestCheckResourceAttr(testIpIpsecSettings, "interim_update", "10s"), + ), + }, + { + Config: testAccIpIpsecSettingsConfig("false", "0s"), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecSettings), + resource.TestCheckResourceAttr(testIpIpsecSettings, "xauth_use_radius", "false"), + resource.TestCheckResourceAttr(testIpIpsecSettings, "interim_update", "0s"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpIpsecSettingsConfig(param1, param2 string) string { + return fmt.Sprintf(`%v + +resource "routeros_ip_ipsec_settings" "test" { + xauth_use_radius = %v + interim_update = "%v" +} +`, providerConfig, param1, param2) +} From f93bf1cd06062424dc5c75303ed4538ead206c72 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 10:50:59 +0300 Subject: [PATCH 16/24] chore: Fix the boilerplate --- examples/resources/routeros_ip_ipsec_key/import.sh | 2 +- examples/resources/routeros_ip_ipsec_mode_config/import.sh | 2 +- examples/resources/routeros_ip_ipsec_policy_group/import.sh | 2 +- examples/resources/routeros_ip_ipsec_profile/import.sh | 2 +- tools/boilerplate/main.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/resources/routeros_ip_ipsec_key/import.sh b/examples/resources/routeros_ip_ipsec_key/import.sh index 41c4ef61..0388db94 100644 --- a/examples/resources/routeros_ip_ipsec_key/import.sh +++ b/examples/resources/routeros_ip_ipsec_key/import.sh @@ -1,5 +1,5 @@ #The ID can be found via API or the terminal #The command for the terminal is -> :put [/ip/ipsec/key get [print show-ids]] terraform import routeros_ip_ipsec_key.test *3 -#Or you can import a certificate using one of its attributes +#Or you can import a resource using one of its attributes terraform import routeros_ip_ipsec_key.test "name=test-key" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_mode_config/import.sh b/examples/resources/routeros_ip_ipsec_mode_config/import.sh index 84913c2c..48f7610e 100644 --- a/examples/resources/routeros_ip_ipsec_mode_config/import.sh +++ b/examples/resources/routeros_ip_ipsec_mode_config/import.sh @@ -1,5 +1,5 @@ #The ID can be found via API or the terminal #The command for the terminal is -> :put [/ip/ipsec/mode/config get [print show-ids]] terraform import routeros_ip_ipsec_mode_config.test *3 -#Or you can import a certificate using one of its attributes +#Or you can import a resource using one of its attributes terraform import routeros_ip_ipsec_mode_config.test "address=1.2.3.4" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_policy_group/import.sh b/examples/resources/routeros_ip_ipsec_policy_group/import.sh index da29b4aa..4c95e437 100644 --- a/examples/resources/routeros_ip_ipsec_policy_group/import.sh +++ b/examples/resources/routeros_ip_ipsec_policy_group/import.sh @@ -1,5 +1,5 @@ #The ID can be found via API or the terminal #The command for the terminal is -> :put [/ip/ipsec/policy/group get [print show-ids]] terraform import routeros_ip_ipsec_policy_group.test *3 -#Or you can import a certificate using one of its attributes +#Or you can import a resource using one of its attributes terraform import routeros_ip_ipsec_policy_group.test "name=test-group" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_profile/import.sh b/examples/resources/routeros_ip_ipsec_profile/import.sh index c07d41e2..48e23b0c 100644 --- a/examples/resources/routeros_ip_ipsec_profile/import.sh +++ b/examples/resources/routeros_ip_ipsec_profile/import.sh @@ -1,5 +1,5 @@ #The ID can be found via API or the terminal #The command for the terminal is -> :put [/ip/ipsec/profile get [print show-ids]] terraform import routeros_ip_ipsec_profile.test *3 -#Or you can import a certificate using one of its attributes +#Or you can import a resource using one of its attributes terraform import routeros_ip_ipsec_profile.test "name=test-profile" \ No newline at end of file diff --git a/tools/boilerplate/main.go b/tools/boilerplate/main.go index 2bf4b80e..13c67227 100644 --- a/tools/boilerplate/main.go +++ b/tools/boilerplate/main.go @@ -202,7 +202,7 @@ Usage: go run tools/bolerplate/main.go [-from-csv] [-table file.csv] [-system] var exampleImportFile = `#The ID can be found via API or the terminal #The command for the terminal is -> :put [/{{.ResourcePath}} get [print show-ids]] terraform import {{.ResourceName}}.test *3 -#Or you can import a certificate using one of its attributes +#Or you can import a resource using one of its attributes terraform import {{.ResourceName}}.test "name=xxx"` var exampleResourceFile = ` From 7600d457e5e4245c9c743f2f28d42194c2a0f0e8 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 10:51:38 +0300 Subject: [PATCH 17/24] feat(ipsec): New resource `routeros_ip_ipsec_peer` --- .../routeros_ip_ipsec_peer/import.sh | 5 + .../routeros_ip_ipsec_peer/resource.tf | 5 + routeros/provider.go | 1 + routeros/resource_ip_ipsec_peer.go | 106 ++++++++++++++++++ routeros/resource_ip_ipsec_peer_test.go | 51 +++++++++ 5 files changed, 168 insertions(+) create mode 100644 examples/resources/routeros_ip_ipsec_peer/import.sh create mode 100644 examples/resources/routeros_ip_ipsec_peer/resource.tf create mode 100644 routeros/resource_ip_ipsec_peer.go create mode 100644 routeros/resource_ip_ipsec_peer_test.go diff --git a/examples/resources/routeros_ip_ipsec_peer/import.sh b/examples/resources/routeros_ip_ipsec_peer/import.sh new file mode 100644 index 00000000..91120a98 --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_peer/import.sh @@ -0,0 +1,5 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/ipsec/peer get [print show-ids]] +terraform import routeros_ip_ipsec_peer.test *3 +#Or you can import a resource using one of its attributes +terraform import routeros_ip_ipsec_peer.test "name=NordVPN" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_peer/resource.tf b/examples/resources/routeros_ip_ipsec_peer/resource.tf new file mode 100644 index 00000000..ca085e3b --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_peer/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_ip_ipsec_peer" "test" { + address = "lv20.nordvpn.com" + exchange_mode = "ike2" + name = "NordVPN" +} diff --git a/routeros/provider.go b/routeros/provider.go index f53beb23..33334c29 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -260,6 +260,7 @@ func Provider() *schema.Provider { // VPN "routeros_ip_ipsec_key": ResourceIpIpsecKey(), "routeros_ip_ipsec_mode_config": ResourceIpIpsecModeConfig(), + "routeros_ip_ipsec_peer": ResourceIpIpsecPeer(), "routeros_ip_ipsec_policy_group": ResourceIpIpsecPolicyGroup(), "routeros_ip_ipsec_profile": ResourceIpIpsecProfile(), "routeros_ip_ipsec_settings": ResourceIpIpsecSettings(), diff --git a/routeros/resource_ip_ipsec_peer.go b/routeros/resource_ip_ipsec_peer.go new file mode 100644 index 00000000..8dcfb3eb --- /dev/null +++ b/routeros/resource_ip_ipsec_peer.go @@ -0,0 +1,106 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*1", + "disabled": "false", + "dynamic": "false", + "exchange-mode": "main", + "name": "peer1", + "passive": "true", + "profile": "default", + "responder": "true", + "send-initial-contact": "true" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/IPsec#IPsec-Peers +func ResourceIpIpsecPeer() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/ipsec/peer"), + MetaId: PropId(Id), + + "address": { + Type: schema.TypeString, + Optional: true, + Description: "If the remote peer's address matches this prefix, then the peer configuration is used in authentication " + + "and establishment of Phase 1. If several peer's addresses match several configuration entries, the most " + + "specific one (i.e. the one with the largest netmask) will be used.", + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + KeyDynamic: PropDynamicRo, + "exchange_mode": { + Type: schema.TypeString, + Optional: true, + Description: "Different ISAKMP phase 1 exchange modes according to RFC 2408. the main mode relaxes rfc2409 " + + "section 5.4, to allow pre-shared-key authentication in the main mode. ike2 mode enables Ikev2 RFC 7296. " + + "Parameters that are ignored by IKEv2 proposal-check, compatibility-options, lifebytes, dpd-maximum-failures, " + + "nat-traversal.", + ValidateFunc: validation.StringInSlice([]string{"aggressive", "base", "main", "ike2"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "local_address": { + Type: schema.TypeString, + Optional: true, + Description: "Routers local address on which Phase 1 should be bounded to.", + }, + KeyName: PropName("Peer name."), + "passive": { + Type: schema.TypeBool, + Optional: true, + Description: "When a passive mode is enabled will wait for a remote peer to initiate an IKE connection. " + + "The enabled passive mode also indicates that the peer is xauth responder, and disabled passive mode " + + "- xauth initiator. When a passive mode is a disabled peer will try to establish not only phase1 but " + + "also phase2 automatically, if policies are configured or created during the phase1.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "port": { + Type: schema.TypeInt, + Optional: true, + Description: "Communication port used (when a router is an initiator) to connect to remote peer in cases " + + "if remote peer uses the non-default port.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "profile": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the profile template that will be used during IKE negotiation.", + ValidateFunc: validation.StringInSlice([]string{"string"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "responder": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether this peer will act as a responder only (listen to incoming requests) and not " + + "initiate a connection.", + }, + "send_initial_contact": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies whether to send `initial contact` IKE packet or wait for remote side, this packet " + + "should trigger the removal of old peer SAs for current source address. Usually, in road warrior setups " + + "clients are initiators and this parameter should be set to no. Initial contact is not sent if modecfg " + + "or xauth is enabled for ikev1.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: ImportStateCustomContext(resSchema), + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_ipsec_peer_test.go b/routeros/resource_ip_ipsec_peer_test.go new file mode 100644 index 00000000..f614acdb --- /dev/null +++ b/routeros/resource_ip_ipsec_peer_test.go @@ -0,0 +1,51 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpIpsecPeer = "routeros_ip_ipsec_peer.test" + +func TestAccIpIpsecPeerTest_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/ipsec/peer", "routeros_ip_ipsec_peer"), + Steps: []resource.TestStep{ + { + Config: testAccIpIpsecPeerConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecPeer), + resource.TestCheckResourceAttr(testIpIpsecPeer, "address", "lv20.nordvpn.com"), + resource.TestCheckResourceAttr(testIpIpsecPeer, "exchange_mode", "ike2"), + resource.TestCheckResourceAttr(testIpIpsecPeer, "name", "NordVPN"), + resource.TestCheckResourceAttr(testIpIpsecPeer, "profile", "default"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpIpsecPeerConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_ipsec_peer" "test" { + address = "lv20.nordvpn.com" + exchange_mode = "ike2" + name = "NordVPN" +} + +`, providerConfig) +} From 0ee11f44c1c5f0fbb994a65cd3a2423b2216489a Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 12:38:53 +0300 Subject: [PATCH 18/24] refactor: Fix the return of a single MT item. --- routeros/mikrotik.go | 9 +++++++++ routeros/mikrotik_client_rest.go | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/routeros/mikrotik.go b/routeros/mikrotik.go index f8882f8c..f42ef972 100644 --- a/routeros/mikrotik.go +++ b/routeros/mikrotik.go @@ -62,6 +62,15 @@ func (m MikrotikItem) GetID(t IdType) string { return "" } +func (m *MikrotikItem) replace(swap any) { + switch t := swap.(type) { + case *MikrotikItem: + *m = *t + default: + panic("not the same type") + } +} + // KebabToSnake Convert Mikrotik JSON names to TF schema names: some-filed to some_field. func KebabToSnake(name string) string { res := []byte(name) diff --git a/routeros/mikrotik_client_rest.go b/routeros/mikrotik_client_rest.go index ca5406f5..264e5b70 100644 --- a/routeros/mikrotik_client_rest.go +++ b/routeros/mikrotik_client_rest.go @@ -131,7 +131,10 @@ func (c *RestClient) SendRequest(method crudMethod, url *URL, item MikrotikItem, } if result != nil && !isSlice(result) && len(*slice) > 0 { - result = &(*slice)[0] + // result.(*MikrotikItem).replace(&(*slice)[0]) + for k, v := range (*slice)[0] { + (*result.(*MikrotikItem))[k] = v + } } return nil From 6f61879ef8e84254fbf0a602ec8a09b714bc58be Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 12:39:29 +0300 Subject: [PATCH 19/24] fix(ipsec): Add the lost attributes --- routeros/resource_ip_ipsec_mode_config.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/routeros/resource_ip_ipsec_mode_config.go b/routeros/resource_ip_ipsec_mode_config.go index d8434a27..93fd4c0f 100644 --- a/routeros/resource_ip_ipsec_mode_config.go +++ b/routeros/resource_ip_ipsec_mode_config.go @@ -45,6 +45,12 @@ func ResourceIpIpsecModeConfig() *schema.Resource { ValidateFunc: validation.IntBetween(1, 32), DiffSuppressFunc: AlwaysPresentNotUserProvided, }, + "connection_mark": { + Type: schema.TypeString, + Optional: true, + Description: "Firewall connection mark.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, // KeyComment: PropCommentRw, KeyName: PropName(""), "responder": { @@ -90,6 +96,13 @@ func ResourceIpIpsecModeConfig() *schema.Resource { Description: "When this option is enabled DNS addresses will be taken from `/ip dns`.", DiffSuppressFunc: AlwaysPresentNotUserProvided, }, + "use_responder_dns": { + Type: schema.TypeString, + Optional: true, + Description: "", + ValidateFunc: validation.StringInSlice([]string{"exclusively", "yes", "no"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, } return &schema.Resource{ From afdbadb7297560dc97d07cbd0abe8d1f7fa0fb9f Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 12:39:58 +0300 Subject: [PATCH 20/24] feat(ipsec): Add new resource `routeros_ip_ipsec_identity` --- .../routeros_ip_ipsec_identity/import.sh | 5 + .../routeros_ip_ipsec_identity/resource.tf | 21 ++ routeros/provider.go | 1 + routeros/resource_ip_ipsec_identity.go | 192 ++++++++++++++++++ routeros/resource_ip_ipsec_identity_test.go | 83 ++++++++ 5 files changed, 302 insertions(+) create mode 100644 examples/resources/routeros_ip_ipsec_identity/import.sh create mode 100644 examples/resources/routeros_ip_ipsec_identity/resource.tf create mode 100644 routeros/resource_ip_ipsec_identity.go create mode 100644 routeros/resource_ip_ipsec_identity_test.go diff --git a/examples/resources/routeros_ip_ipsec_identity/import.sh b/examples/resources/routeros_ip_ipsec_identity/import.sh new file mode 100644 index 00000000..4afdf2e0 --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_identity/import.sh @@ -0,0 +1,5 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/ipsec/identity get [print show-ids]] +terraform import routeros_ip_ipsec_identity.test *3 +#Or you can import a resource using one of its attributes +terraform import routeros_ip_ipsec_identity.test "peer=NordVPN" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_identity/resource.tf b/examples/resources/routeros_ip_ipsec_identity/resource.tf new file mode 100644 index 00000000..2db57c31 --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_identity/resource.tf @@ -0,0 +1,21 @@ +resource "routeros_ip_ipsec_mode_config" "test" { + name = "NordVPN" + responder = false +} + +resource "routeros_ip_ipsec_peer" "test" { + address = "lv20.nordvpn.com" + exchange_mode = "ike2" + name = "NordVPN" +} + +resource "routeros_ip_ipsec_identity" "test" { + auth-method = "eap" + certificate = "" + eap-methods = "eap-mschapv2" + generate-policy = "port-strict" + mode-config = routeros_ip_ipsec_mode_config.test.name + peer = routeros_ip_ipsec_peer.test.name + username = "support@mikrotik.com" + password = "secret" +} diff --git a/routeros/provider.go b/routeros/provider.go index 33334c29..1a8b0f78 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -258,6 +258,7 @@ func Provider() *schema.Provider { "routeros_routing_ospf_interface_template": ResourceRoutingOspfInterfaceTemplate(), // VPN + "routeros_ip_ipsec_identity": ResourceIpIpsecIdentity(), "routeros_ip_ipsec_key": ResourceIpIpsecKey(), "routeros_ip_ipsec_mode_config": ResourceIpIpsecModeConfig(), "routeros_ip_ipsec_peer": ResourceIpIpsecPeer(), diff --git a/routeros/resource_ip_ipsec_identity.go b/routeros/resource_ip_ipsec_identity.go new file mode 100644 index 00000000..37440711 --- /dev/null +++ b/routeros/resource_ip_ipsec_identity.go @@ -0,0 +1,192 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*A", + "auth-method": "pre-shared-key", + "disabled": "false", + "dynamic": "false", + "generate-policy": "no", + "peer": "peer1", + "secret": "secret!!!" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/IPsec#IPsec-Identities +func ResourceIpIpsecIdentity() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/ipsec/identity"), + MetaId: PropId(Id), + + "auth_method": { + Type: schema.TypeString, + Optional: true, + Description: "Authentication method: `digital-signature` - authenticate using a pair of RSA certificates; `eap` " + + "- IKEv2 EAP authentication for initiator (peer with a netmask of `/32`). Must be used together with eap-methods; `eap-radius` " + + "- IKEv2 EAP RADIUS passthrough authentication for the responder (RFC 3579). A server certificate in " + + "this case is required. If a server certificate is not specified then only clients supporting EAP-only " + + "(RFC 5998) will be able to connect. Note that the EAP method should be compatible with EAP-only; `pre-shared-key` " + + "- authenticate by a password (pre-shared secret) string shared between the peers (not recommended since " + + "an offline attack on the pre-shared key is possible); `rsa-key` - authenticate using an RSA key imported " + + "in keys menu. Only supported in IKEv1; `pre-shared-key-xauth` - authenticate by a password (pre-shared " + + "secret) string shared between the peers + XAuth username and password. Only supported in IKEv1; `rsa-signature-hybrid` " + + "- responder certificate authentication with initiator XAuth. Only supported in IKEv1.", + ValidateFunc: validation.StringInSlice([]string{"digital-signature", "eap", "eap-radius", "`pre-shared-key`", + "pre-shared-key-xauth", "rsa-key", "rsa-signature-hybrid"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "certificate": { + Type: schema.TypeString, + Optional: true, + Description: "Name of a certificate listed in System/Certificates (signing packets; the certificate must " + + "have the private key). Applicable if digital signature authentication method (`auth-method=digital-signature`) " + + "or EAP (a`uth-method=eap`) is used.", + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + KeyDynamic: PropDynamicRo, + "eap_methods": { + Type: schema.TypeString, + Optional: true, + Description: "All EAP methods requires whole certificate chain including intermediate and root CA certificates " + + "to be present in System/Certificates menu. Also, the username and password (if required by the authentication " + + "server) must be specified. Multiple EAP methods may be specified and will be used in a specified order. " + + "Currently supported EAP methods: `eap-mschapv2`; `eap-peap` - also known as PEAPv0/EAP-MSCHAPv2; `eap-tls` - " + + "requires additional client certificate specified under certificate parameter; `eap-ttls`.", + ValidateFunc: validation.StringInSlice([]string{"eap-mschapv2", "eap-peap", "eap-tls", "eap-ttls"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "generate_policy": { + Type: schema.TypeString, + Optional: true, + Description: "Allow this peer to establish SA for non-existing policies. Such policies are created dynamically " + + "for the lifetime of SA. Automatic policies allows, for example, to create IPsec secured L2TP tunnels, " + + "or any other setup where remote peer's IP address is not known at the configuration time. `no` - do not " + + "generate policies; `port-override` - generate policies and force policy to use any port (old behavior); " + + "`port-strict` - use ports from peer's proposal, which should match peer's policy.", + ValidateFunc: validation.StringInSlice([]string{"no", "port-override", "port-strict"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "key": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the private key from keys menu. Applicable if RSA key authentication method (`auth-method=rsa-key`) " + + "is used.", + }, + "match_by": { + Type: schema.TypeString, + Optional: true, + Description: "Defines the logic used for peer's identity validation. `remote-id` - will verify the peer's ID " + + "according to remote-id setting. `certificate` will verify the peer's certificate with what is specified " + + "under remote-certificate setting.", + ValidateFunc: validation.StringInSlice([]string{"remote-id", "certificate"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "mode_config": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the configuration parameters from mode-config menu. When parameter is set mode-config " + + "is enabled.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "my_id": { + Type: schema.TypeString, + Optional: true, + Description: "On initiator, this controls what ID_i is sent to the responder. On responder, this controls " + + "what ID_r is sent to the initiator. In IKEv2, responder also expects this ID in received ID_r from initiator. `auto` " + + "- tries to use correct ID automatically: IP for pre-shared key, SAN (DN if not present) for certificate " + + "based connections; `address` - IP address is used as ID;dn - the binary Distinguished Encoding Rules (DER) " + + "encoding of an ASN.1 X.500 Distinguished Name; `fqdn` - fully qualified domain name; `key-id` - use the specified " + + "key ID for the identity; `user-fqdn` - specifies a fully-qualified username string, for example, `user@domain.com`.", + ValidateFunc: validation.StringInSlice([]string{"auto", "address", "fqdn", "user-fqdn", "key-id"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "notrack_chain": { + Type: schema.TypeString, + Optional: true, + Description: "Adds IP/Firewall/Raw rules matching IPsec policy to a specified chain. Use together with generate-policy.", + }, + "password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "XAuth or EAP password. Applicable if pre-shared key with XAuth authentication method " + + "(`auth-method=pre-shared-key-xauth`) or EAP (`auth-method=eap`) is used.", + }, + "peer": { + Type: schema.TypeString, + Required: true, + Description: "Name of the peer on which the identity applies.", + }, + "policy_template_group": { + Type: schema.TypeString, + Optional: true, + Description: "If generate-policy is enabled, traffic selectors are checked against templates from the same " + + "group. If none of the templates match, Phase 2 SA will not be established.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "remote_certificate": { + Type: schema.TypeString, + Optional: true, + Description: "Name of a certificate (listed in `System/Certificates`) for authenticating the remote side (validating " + + "packets; no private key required). If a remote-certificate is not specified then the received certificate " + + "from a remote peer is used and checked against CA in the certificate menu. Proper CA must be imported " + + "in a certificate store. If remote-certificate and match-by=certificate is specified, only the specific " + + "client certificate will be matched. Applicable if digital signature authentication method " + + "(`auth-method=digital-signature`) is used.", + }, + "remote_id": { + Type: schema.TypeString, + Optional: true, + Description: "This parameter controls what ID value to expect from the remote peer. Note that all types " + + "except for ignoring will verify remote peer's ID with a received certificate. In case when the peer " + + "sends the certificate name as its ID, it is checked against the certificate, else the ID is checked " + + "against Subject Alt. Name. `auto` - accept all ID's;address - IP address is used as ID;dn - the binary " + + "Distinguished Encoding Rules (DER) encoding of an ASN.1 X.500 Distinguished Name; `fqdn` - fully qualified " + + "domain name. Only supported in IKEv2; `user-fqdn` - a fully-qualified username string, for example, `user@domain.com`. " + + "Only supported in IKEv2; `key-id` - specific key ID for the identity. Only supported in IKEv2; `ignore` - " + + "do not verify received ID with certificate (dangerous). * Wildcard key ID matching **is not supported**, " + + "for example `remote-id=`key-id:CN=*.domain.com`.", + ValidateFunc: validation.StringInSlice([]string{"auto", "fqdn", "user-fqdn", "key-id", "ignore"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "remote_key": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the public key from keys menu. Applicable if RSA key authentication method " + + "(`auth-method=rsa-key`) is used.", + }, + "secret": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Secret string. If it starts with '0x', it is parsed as a hexadecimal value. Applicable if " + + "pre-shared key authentication method (`auth-method=pre-shared-key` and `auth-method=pre-shared-key-xauth`) " + + "is used.", + }, + "username": { + Type: schema.TypeString, + Optional: true, + Description: "XAuth or EAP username. Applicable if pre-shared key with XAuth authentication method " + + "(`auth-method=pre-shared-key-xauth`) or EAP (`auth-method=eap`) is used.", + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: ImportStateCustomContext(resSchema), + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_ipsec_identity_test.go b/routeros/resource_ip_ipsec_identity_test.go new file mode 100644 index 00000000..a41bce1c --- /dev/null +++ b/routeros/resource_ip_ipsec_identity_test.go @@ -0,0 +1,83 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +const testIpIpsecIdentity = "routeros_ip_ipsec_identity.test" + +func TestAccIpIpsecIdentityTest_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/ipsec/identity", "routeros_ip_ipsec_identity"), + Steps: []resource.TestStep{ + { + Config: testAccIpIpsecIdentityConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecIdentity), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "auth_method", "eap"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "certificate", ""), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "eap_methods", "eap-mschapv2"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "generate_policy", "port-strict"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "mode_config", "NordVPN"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "peer", "NordVPN"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "username", "support@mikrotik.com"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "password", "secret"), + ), + }, + { + Config: testAccIpIpsecIdentityConfig(), + ResourceName: testIpIpsecIdentity, + ImportStateId: `peer=NordVPN`, + ImportState: true, + ImportStateCheck: func(states []*terraform.InstanceState) error { + if len(states) != 1 { + return fmt.Errorf("more than 1 states received, only one expected") + } + return nil + }, + }, + }, + }) + + }) + } +} + +func testAccIpIpsecIdentityConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_ipsec_mode_config" "test" { + name = "NordVPN" + responder = false +} + +resource "routeros_ip_ipsec_peer" "test" { + address = "lv20.nordvpn.com" + exchange_mode = "ike2" + name = "NordVPN" +} + +resource "routeros_ip_ipsec_identity" "test" { + auth_method = "eap" + certificate = "" + eap_methods = "eap-mschapv2" + generate_policy = "port-strict" + mode_config = routeros_ip_ipsec_mode_config.test.name + peer = routeros_ip_ipsec_peer.test.name + username = "support@mikrotik.com" + password = "secret" +} +`, providerConfig) +} From 0126d3ab655275e16e146dc3292dd9e244b1a512 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 13:27:32 +0300 Subject: [PATCH 21/24] test: Set unique resource names for this test --- routeros/resource_ip_ipsec_identity_test.go | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/routeros/resource_ip_ipsec_identity_test.go b/routeros/resource_ip_ipsec_identity_test.go index a41bce1c..d49114cf 100644 --- a/routeros/resource_ip_ipsec_identity_test.go +++ b/routeros/resource_ip_ipsec_identity_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/terraform" ) -const testIpIpsecIdentity = "routeros_ip_ipsec_identity.test" +const testIpIpsecIdentity = "routeros_ip_ipsec_identity.identity" func TestAccIpIpsecIdentityTest_basic(t *testing.T) { t.Parallel() @@ -30,8 +30,8 @@ func TestAccIpIpsecIdentityTest_basic(t *testing.T) { resource.TestCheckResourceAttr(testIpIpsecIdentity, "certificate", ""), resource.TestCheckResourceAttr(testIpIpsecIdentity, "eap_methods", "eap-mschapv2"), resource.TestCheckResourceAttr(testIpIpsecIdentity, "generate_policy", "port-strict"), - resource.TestCheckResourceAttr(testIpIpsecIdentity, "mode_config", "NordVPN"), - resource.TestCheckResourceAttr(testIpIpsecIdentity, "peer", "NordVPN"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "mode_config", "NordVPN-i"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "peer", "NordVPN-i"), resource.TestCheckResourceAttr(testIpIpsecIdentity, "username", "support@mikrotik.com"), resource.TestCheckResourceAttr(testIpIpsecIdentity, "password", "secret"), ), @@ -39,7 +39,7 @@ func TestAccIpIpsecIdentityTest_basic(t *testing.T) { { Config: testAccIpIpsecIdentityConfig(), ResourceName: testIpIpsecIdentity, - ImportStateId: `peer=NordVPN`, + ImportStateId: `peer=NordVPN-i`, ImportState: true, ImportStateCheck: func(states []*terraform.InstanceState) error { if len(states) != 1 { @@ -58,24 +58,24 @@ func TestAccIpIpsecIdentityTest_basic(t *testing.T) { func testAccIpIpsecIdentityConfig() string { return fmt.Sprintf(`%v -resource "routeros_ip_ipsec_mode_config" "test" { - name = "NordVPN" +resource "routeros_ip_ipsec_mode_config" "mode-for-identity" { + name = "NordVPN-i" responder = false } -resource "routeros_ip_ipsec_peer" "test" { +resource "routeros_ip_ipsec_peer" "peer-for-identity" { address = "lv20.nordvpn.com" exchange_mode = "ike2" - name = "NordVPN" + name = "NordVPN-i" } -resource "routeros_ip_ipsec_identity" "test" { +resource "routeros_ip_ipsec_identity" "identity" { auth_method = "eap" certificate = "" eap_methods = "eap-mschapv2" generate_policy = "port-strict" - mode_config = routeros_ip_ipsec_mode_config.test.name - peer = routeros_ip_ipsec_peer.test.name + mode_config = routeros_ip_ipsec_mode_config.mode-for-identity.name + peer = routeros_ip_ipsec_peer.peer-for-identity.name username = "support@mikrotik.com" password = "secret" } From 9fee803c5a19ad244d63ff8f052dc6c9de90c47f Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 13:42:40 +0300 Subject: [PATCH 22/24] feat(ipsec): Add new resource `routeros_ip_ipsec_proposal` --- .../routeros_ip_ipsec_proposal/import.sh | 5 ++ .../routeros_ip_ipsec_proposal/resource.tf | 5 ++ routeros/provider.go | 1 + routeros/resource_ip_ipsec_proposal.go | 82 +++++++++++++++++++ routeros/resource_ip_ipsec_proposal_test.go | 49 +++++++++++ 5 files changed, 142 insertions(+) create mode 100644 examples/resources/routeros_ip_ipsec_proposal/import.sh create mode 100644 examples/resources/routeros_ip_ipsec_proposal/resource.tf create mode 100644 routeros/resource_ip_ipsec_proposal.go create mode 100644 routeros/resource_ip_ipsec_proposal_test.go diff --git a/examples/resources/routeros_ip_ipsec_proposal/import.sh b/examples/resources/routeros_ip_ipsec_proposal/import.sh new file mode 100644 index 00000000..8511477d --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_proposal/import.sh @@ -0,0 +1,5 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/ipsec/proposal get [print show-ids]] +terraform import routeros_ip_ipsec_proposal.test *3 +#Or you can import a resource using one of its attributes +terraform import routeros_ip_ipsec_proposal.test "name=NordVPN" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_proposal/resource.tf b/examples/resources/routeros_ip_ipsec_proposal/resource.tf new file mode 100644 index 00000000..24e0e73b --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_proposal/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_ip_ipsec_proposal" "test" { + name = "NordVPN" + pfs_group = "none" + lifetime = "45m10s" +} diff --git a/routeros/provider.go b/routeros/provider.go index 1a8b0f78..49f645c5 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -264,6 +264,7 @@ func Provider() *schema.Provider { "routeros_ip_ipsec_peer": ResourceIpIpsecPeer(), "routeros_ip_ipsec_policy_group": ResourceIpIpsecPolicyGroup(), "routeros_ip_ipsec_profile": ResourceIpIpsecProfile(), + "routeros_ip_ipsec_proposal": ResourceIpIpsecProposal(), "routeros_ip_ipsec_settings": ResourceIpIpsecSettings(), "routeros_ovpn_server": ResourceOpenVPNServer(), diff --git a/routeros/resource_ip_ipsec_proposal.go b/routeros/resource_ip_ipsec_proposal.go new file mode 100644 index 00000000..c244da68 --- /dev/null +++ b/routeros/resource_ip_ipsec_proposal.go @@ -0,0 +1,82 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*0", + "auth-algorithms": "sha1", + "default": "true", + "disabled": "false", + "enc-algorithms": "aes-256-cbc,aes-192-cbc,aes-128-cbc", + "lifetime": "30m", + "name": "default", + "pfs-group": "modp1024" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/IPsec#IPsec-Proposals +func ResourceIpIpsecProposal() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/ipsec/proposal"), + MetaId: PropId(Id), + + "auth_algorithms": { + Type: schema.TypeSet, + Optional: true, + Description: "Allowed algorithms for authorization. SHA (Secure Hash Algorithm) is stronger but slower. " + + "MD5 uses a 128-bit key, sha1-160bit key.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"md5", "null", "sha1", "sha256", "sha512"}, false), + }, + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + KeyComment: PropCommentRw, + KeyDefault: PropDefaultRo, + KeyDisabled: PropDisabledRw, + "enc_algorithms": { + Type: schema.TypeSet, + Optional: true, + Description: "Allowed algorithms and key lengths to use for SAs.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"null", "des", "3des", "aes-128-cbc", "aes-128-cbc", + "aes-128gcm", "aes-192-cbc", "aes-192-ctr", "aes-192-gcm", "aes-256-cbc", "aes-256-ctr", "aes-256-gcm", + "blowfish", "camellia-128", "camellia-192", "camellia-256", "twofish"}, false), + }, + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "lifetime": { + Type: schema.TypeString, + Optional: true, + Description: "How long to use SA before throwing it out.", + DiffSuppressFunc: TimeEquall, + }, + KeyName: PropName(""), + "pfs_group": { + Type: schema.TypeString, + Optional: true, + Description: "The diffie-Helman group used for Perfect Forward Secrecy.", + ValidateFunc: validation.StringInSlice([]string{"ecp256", "ecp384", "ecp521", "modp768", "modp1024", + "modp1536", "modp2048", "modp3072", "modp4096", "modp6144", "modp8192", "none"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: ImportStateCustomContext(resSchema), + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_ipsec_proposal_test.go b/routeros/resource_ip_ipsec_proposal_test.go new file mode 100644 index 00000000..944f1b01 --- /dev/null +++ b/routeros/resource_ip_ipsec_proposal_test.go @@ -0,0 +1,49 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const testIpIpsecProposal = "routeros_ip_ipsec_proposal.test" + +func TestAccIpIpsecProposalTest_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/ipsec/proposal", "routeros_ip_ipsec_proposal"), + Steps: []resource.TestStep{ + { + Config: testAccIpIpsecProposalConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecProposal), + resource.TestCheckResourceAttr(testIpIpsecProposal, "name", "NordVPN"), + resource.TestCheckResourceAttr(testIpIpsecProposal, "pfs_group", "none"), + resource.TestCheckResourceAttr(testIpIpsecProposal, "lifetime", "45m10s"), + ), + }, + }, + }) + + }) + } +} + +func testAccIpIpsecProposalConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_ipsec_proposal" "test" { + name = "NordVPN" + pfs_group = "none" + lifetime = "45m10s" +} +`, providerConfig) +} From 9ba2bf961a0d7dafaf5d778a10f0a2e153f0d666 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 14:15:23 +0300 Subject: [PATCH 23/24] feat(ipsec): Add new resource `routeros_ip_ipsec_policy` --- .../routeros_ip_ipsec_policy/import.sh | 5 + .../routeros_ip_ipsec_policy/resource.tf | 11 ++ routeros/provider.go | 1 + routeros/resource_ip_ipsec_policy.go | 156 ++++++++++++++++++ routeros/resource_ip_ipsec_policy_test.go | 70 ++++++++ 5 files changed, 243 insertions(+) create mode 100644 examples/resources/routeros_ip_ipsec_policy/import.sh create mode 100644 examples/resources/routeros_ip_ipsec_policy/resource.tf create mode 100644 routeros/resource_ip_ipsec_policy.go create mode 100644 routeros/resource_ip_ipsec_policy_test.go diff --git a/examples/resources/routeros_ip_ipsec_policy/import.sh b/examples/resources/routeros_ip_ipsec_policy/import.sh new file mode 100644 index 00000000..d4b6e3ac --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_policy/import.sh @@ -0,0 +1,5 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/ip/ipsec/policy get [print show-ids]] +terraform import routeros_ip_ipsec_policy.test *3 +#Or you can import a resource using one of its attributes +terraform import routeros_ip_ipsec_policy.test "group=test-group" \ No newline at end of file diff --git a/examples/resources/routeros_ip_ipsec_policy/resource.tf b/examples/resources/routeros_ip_ipsec_policy/resource.tf new file mode 100644 index 00000000..1a08750f --- /dev/null +++ b/examples/resources/routeros_ip_ipsec_policy/resource.tf @@ -0,0 +1,11 @@ +resource "routeros_ip_ipsec_policy_group" "group-for-policy" { + name = "test-group" +} + +resource "routeros_ip_ipsec_policy" "policy" { + dst_address = "0.0.0.0/0" + group = routeros_ip_ipsec_policy_group.group-for-policy.name + proposal = "NordVPN" + src_address = "0.0.0.0/0" + template = true +} diff --git a/routeros/provider.go b/routeros/provider.go index 49f645c5..22a2af9c 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -262,6 +262,7 @@ func Provider() *schema.Provider { "routeros_ip_ipsec_key": ResourceIpIpsecKey(), "routeros_ip_ipsec_mode_config": ResourceIpIpsecModeConfig(), "routeros_ip_ipsec_peer": ResourceIpIpsecPeer(), + "routeros_ip_ipsec_policy": ResourceIpIpsecPolicy(), "routeros_ip_ipsec_policy_group": ResourceIpIpsecPolicyGroup(), "routeros_ip_ipsec_profile": ResourceIpIpsecProfile(), "routeros_ip_ipsec_proposal": ResourceIpIpsecProposal(), diff --git a/routeros/resource_ip_ipsec_policy.go b/routeros/resource_ip_ipsec_policy.go new file mode 100644 index 00000000..a5ed7fc0 --- /dev/null +++ b/routeros/resource_ip_ipsec_policy.go @@ -0,0 +1,156 @@ +package routeros + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +/* + { + ".id": "*1000001", + "action": "encrypt", + "active": "false", + "disabled": "false", + "dst-address": "::/0", + "dst-port": "any", + "dynamic": "false", + "invalid": "false", + "ipsec-protocols": "esp", + "level": "require", + "peer": "peer1", + "ph2-count": "0", + "ph2-state": "no-phase2", + "proposal": "default", + "protocol": "all", + "sa-dst-address": "::", + "sa-src-address": "::", + "src-address": "::/0", + "src-port": "any", + "tunnel": "true" + } +*/ + +// https://help.mikrotik.com/docs/display/ROS/IPsec#IPsec-Policies +func ResourceIpIpsecPolicy() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/ip/ipsec/policy"), + MetaId: PropId(Id), + MetaSkipFields: PropSkipFields("ph2_count", "ph2_state", "sa_dst_address", "sa_src_address"), + + "action": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies what to do with the packet matched by the policy.none - pass the packet unchanged.discard " + + "- drop the packet.encrypt - apply transformations specified in this policy and it's SA.", + ValidateFunc: validation.StringInSlice([]string{"discard", "encrypt", "none"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "active": { + Type: schema.TypeBool, + Computed: true, + }, + KeyComment: PropCommentRw, + KeyDisabled: PropDisabledRw, + "dst_address": { + Type: schema.TypeString, + Optional: true, + Description: "Destination address to be matched in packets. Applicable when tunnel mode (`tunnel=yes`) or " + + "template (`template=yes`) is used.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "dst_port": { + Type: schema.TypeString, + Optional: true, + Description: "Destination port to be matched in packets. If set to any all ports will be matched.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + KeyDynamic: PropDynamicRo, + "group": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the policy group to which this **template** is assigned.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + KeyInvalid: PropInvalidRo, + "ipsec_protocols": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies what combination of Authentication Header and Encapsulating Security Payload protocols " + + "you want to apply to matched traffic.", + ValidateFunc: validation.StringInSlice([]string{"ah", "esp"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "level": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies what to do if some of the SAs for this policy cannot be found:use - skip this transform, " + + "do not drop the packet, and do not acquire SA from IKE daemon;require - drop the packet and acquire " + + "SA;unique - drop the packet and acquire a unique SA that is only used with this particular policy. It " + + "is used in setups where multiple clients can sit behind one public IP address (clients behind NAT).", + ValidateFunc: validation.StringInSlice([]string{"require", "unique", "use"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "peer": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the peer on which the policy applies.", + }, + "proposal": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the proposal template that will be sent by IKE daemon to establish SAs for this policy.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "protocol": { + Type: schema.TypeString, + Optional: true, + Description: "IP packet protocol to match.", + ValidateFunc: validation.StringInSlice([]string{"all", "dccp", "ddp", "egp", "encap", "etherip", "ggp", + "gre", "hmp", "icmp", "icmpv6", "idpr-cmtp", "igmp", "ipencap", "ipip", "ipsec-ah", "ipsec-esp", + "ipv6-encap", "ipv6-frag", "ipv6-nonxt", "ipv6-opts", "ipv6-route", "iso-tp4", "l2tp", "ospf", "pim", + "pup", "rdp", "rspf", "rsvp", "sctp", "st", "tcp", "udp", "udp-lite", "vmtp", "vrrp", "xns-idp", "xtp"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "src_address": { + Type: schema.TypeString, + Optional: true, + Description: "Source address to be matched in packets. Applicable when tunnel mode (`tunnel=yes`) or template " + + "(`template=yes`) is used.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "src_port": { + Type: schema.TypeString, + Optional: true, + Description: "Source port to be matched in packets. If set to any all ports will be matched.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "template": { + Type: schema.TypeBool, + Optional: true, + Description: "Creates a template and assigns it to a specified policy group.Following parameters are used " + + "by template: `group` - name of the policy group to which this template is assigned; `src-address`, `dst-address` " + + "- Requested subnet must match in both directions (for example 0.0.0.0/0 to allow all); `protocol` - protocol " + + "to match, if set to all, then any protocol is accepted; `proposal` - SA parameters used for this template; `level` " + + "- useful when unique is required in setups with multiple clients behind NAT.", + }, + "tunnel": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies whether to use tunnel mode.", + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + } + + return &schema.Resource{ + CreateContext: DefaultCreate(resSchema), + ReadContext: DefaultRead(resSchema), + UpdateContext: DefaultUpdate(resSchema), + DeleteContext: DefaultDelete(resSchema), + + Importer: &schema.ResourceImporter{ + StateContext: ImportStateCustomContext(resSchema), + }, + + Schema: resSchema, + } +} diff --git a/routeros/resource_ip_ipsec_policy_test.go b/routeros/resource_ip_ipsec_policy_test.go new file mode 100644 index 00000000..cab3f679 --- /dev/null +++ b/routeros/resource_ip_ipsec_policy_test.go @@ -0,0 +1,70 @@ +package routeros + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +const testIpIpsecPolicy = "routeros_ip_ipsec_policy.policy" + +func TestAccIpIpsecPolicyTest_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/ipsec/policy", "routeros_ip_ipsec_policy"), + Steps: []resource.TestStep{ + { + Config: testAccIpIpsecPolicyConfig(), + Check: resource.ComposeTestCheckFunc( + testResourcePrimaryInstanceId(testIpIpsecPolicy), + resource.TestCheckResourceAttr(testIpIpsecPolicy, "dst_address", "0.0.0.0/0"), + resource.TestCheckResourceAttr(testIpIpsecPolicy, "group", "test-group"), + resource.TestCheckResourceAttr(testIpIpsecPolicy, "proposal", "default"), + resource.TestCheckResourceAttr(testIpIpsecPolicy, "src_address", "0.0.0.0/0"), + resource.TestCheckResourceAttr(testIpIpsecPolicy, "template", "true"), + ), + }, + { + Config: testAccIpIpsecPolicyConfig(), + ResourceName: testIpIpsecPolicy, + ImportStateId: `group=test-group`, + ImportState: true, + ImportStateCheck: func(states []*terraform.InstanceState) error { + if len(states) != 1 { + return fmt.Errorf("more than 1 states received, only one expected") + } + return nil + }, + }, + }, + }) + + }) + } +} + +func testAccIpIpsecPolicyConfig() string { + return fmt.Sprintf(`%v + +resource "routeros_ip_ipsec_policy_group" "group-for-policy" { + name = "test-group" +} + +resource "routeros_ip_ipsec_policy" "policy" { + dst_address = "0.0.0.0/0" + group = routeros_ip_ipsec_policy_group.group-for-policy.name + proposal = "default" + src_address = "0.0.0.0/0" + template = true +} +`, providerConfig) +} From 191f4de93ea7e5b5552a5ed911fc48f2c40354e1 Mon Sep 17 00:00:00 2001 From: Vaerh Date: Mon, 7 Oct 2024 14:43:27 +0300 Subject: [PATCH 24/24] test: Fix tests --- routeros/resource_ip_ipsec_identity_test.go | 2 +- routeros/resource_ip_ipsec_policy_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/routeros/resource_ip_ipsec_identity_test.go b/routeros/resource_ip_ipsec_identity_test.go index d49114cf..8c22bd24 100644 --- a/routeros/resource_ip_ipsec_identity_test.go +++ b/routeros/resource_ip_ipsec_identity_test.go @@ -64,7 +64,7 @@ resource "routeros_ip_ipsec_mode_config" "mode-for-identity" { } resource "routeros_ip_ipsec_peer" "peer-for-identity" { - address = "lv20.nordvpn.com" + address = "lv30.nordvpn.com" exchange_mode = "ike2" name = "NordVPN-i" } diff --git a/routeros/resource_ip_ipsec_policy_test.go b/routeros/resource_ip_ipsec_policy_test.go index cab3f679..c533f1d6 100644 --- a/routeros/resource_ip_ipsec_policy_test.go +++ b/routeros/resource_ip_ipsec_policy_test.go @@ -27,7 +27,7 @@ func TestAccIpIpsecPolicyTest_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testResourcePrimaryInstanceId(testIpIpsecPolicy), resource.TestCheckResourceAttr(testIpIpsecPolicy, "dst_address", "0.0.0.0/0"), - resource.TestCheckResourceAttr(testIpIpsecPolicy, "group", "test-group"), + resource.TestCheckResourceAttr(testIpIpsecPolicy, "group", "test-group-p"), resource.TestCheckResourceAttr(testIpIpsecPolicy, "proposal", "default"), resource.TestCheckResourceAttr(testIpIpsecPolicy, "src_address", "0.0.0.0/0"), resource.TestCheckResourceAttr(testIpIpsecPolicy, "template", "true"), @@ -36,7 +36,7 @@ func TestAccIpIpsecPolicyTest_basic(t *testing.T) { { Config: testAccIpIpsecPolicyConfig(), ResourceName: testIpIpsecPolicy, - ImportStateId: `group=test-group`, + ImportStateId: `group=test-group-p`, ImportState: true, ImportStateCheck: func(states []*terraform.InstanceState) error { if len(states) != 1 { @@ -56,7 +56,7 @@ func testAccIpIpsecPolicyConfig() string { return fmt.Sprintf(`%v resource "routeros_ip_ipsec_policy_group" "group-for-policy" { - name = "test-group" + name = "test-group-p" } resource "routeros_ip_ipsec_policy" "policy" {