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/examples/resources/routeros_ip_ipsec_key/import.sh b/examples/resources/routeros_ip_ipsec_key/import.sh new file mode 100644 index 00000000..0388db94 --- /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 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_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/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..48f7610e --- /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 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_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/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/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/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..4c95e437 --- /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 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_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/examples/resources/routeros_ip_ipsec_profile/import.sh b/examples/resources/routeros_ip_ipsec_profile/import.sh new file mode 100644 index 00000000..48e23b0c --- /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 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/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/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/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/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.go b/routeros/mikrotik_client.go index 06de33a1..947a8fb5 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 } @@ -25,7 +26,8 @@ type Client interface { type crudMethod int const ( - crudCreate crudMethod = iota + crudUnknown crudMethod = iota + crudCreate crudRead crudUpdate crudDelete @@ -38,8 +40,13 @@ const ( crudMove crudStart crudStop + crudGenerateKey ) +type ExtraParams struct { + SuppressSysODelWarn bool +} + func NewClient(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { tlsConf := tls.Config{ @@ -114,6 +121,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,10 +148,16 @@ 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{ - 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 d4f2e138..5dba786e 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 } @@ -33,9 +34,14 @@ var ( crudMove: "/move", crudStart: "/start", crudStop: "/stop", + crudGenerateKey: "/generate-key", } ) +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..264e5b70 100644 --- a/routeros/mikrotik_client_rest.go +++ b/routeros/mikrotik_client_rest.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "reflect" "strings" ) @@ -16,6 +17,7 @@ type RestClient struct { Username string Password string Transport TransportType + extra *ExtraParams *http.Client } @@ -40,9 +42,14 @@ var ( crudMove: "POST", crudStart: "POST", crudStop: "POST", + crudGenerateKey: "POST", } ) +func (c *RestClient) GetExtraParams() *ExtraParams { + return c.extra +} + func (c *RestClient) GetTransport() TransportType { return c.Transport } @@ -97,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 result != nil && 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 { @@ -111,5 +129,32 @@ func (c *RestClient) SendRequest(method crudMethod, url *URL, item MikrotikItem, } } } + + if result != nil && !isSlice(result) && len(*slice) > 0 { + // result.(*MikrotikItem).replace(&(*slice)[0]) + for k, v := range (*slice)[0] { + (*result.(*MikrotikItem))[k] = v + } + } + 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 } diff --git a/routeros/provider.go b/routeros/provider.go index f5fb3c19..22a2af9c 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{ @@ -249,7 +258,16 @@ func Provider() *schema.Provider { "routeros_routing_ospf_interface_template": ResourceRoutingOspfInterfaceTemplate(), // VPN - "routeros_ovpn_server": ResourceOpenVPNServer(), + "routeros_ip_ipsec_identity": ResourceIpIpsecIdentity(), + "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(), + "routeros_ip_ipsec_settings": ResourceIpIpsecSettings(), + "routeros_ovpn_server": ResourceOpenVPNServer(), // PPP "routeros_ppp_aaa": ResourcePppAaa(), 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_default_actions.go b/routeros/resource_actions.go similarity index 50% rename from routeros/resource_default_actions.go rename to routeros/resource_actions.go index 17ac7868..96c2b0a7 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) == "" { @@ -204,126 +348,56 @@ 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("") - 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) + if m.(Client).GetExtraParams().SuppressSysODelWarn { + return nil } + return DeleteSystemObject } -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 +// 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] } } - 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) + res, err := ReadItemsFiltered([]string{SnakeToKebab(fieldName) + "=" + id}, path, m.(Client)) 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) + return nil, 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) + 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 ResourceRead(ctx, s, d, m) + return []*schema.ResourceData{d}, nil } } 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 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..8c22bd24 --- /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.identity" + +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-i"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "peer", "NordVPN-i"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "username", "support@mikrotik.com"), + resource.TestCheckResourceAttr(testIpIpsecIdentity, "password", "secret"), + ), + }, + { + Config: testAccIpIpsecIdentityConfig(), + ResourceName: testIpIpsecIdentity, + ImportStateId: `peer=NordVPN-i`, + 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" "mode-for-identity" { + name = "NordVPN-i" + responder = false +} + +resource "routeros_ip_ipsec_peer" "peer-for-identity" { + address = "lv30.nordvpn.com" + exchange_mode = "ike2" + name = "NordVPN-i" +} + +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.mode-for-identity.name + peer = routeros_ip_ipsec_peer.peer-for-identity.name + username = "support@mikrotik.com" + password = "secret" +} +`, providerConfig) +} 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) +} diff --git a/routeros/resource_ip_ipsec_mode_config.go b/routeros/resource_ip_ipsec_mode_config.go new file mode 100644 index 00000000..93fd4c0f --- /dev/null +++ b/routeros/resource_ip_ipsec_mode_config.go @@ -0,0 +1,120 @@ +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, + }, + "connection_mark": { + Type: schema.TypeString, + Optional: true, + Description: "Firewall connection mark.", + 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, + }, + "use_responder_dns": { + Type: schema.TypeString, + Optional: true, + Description: "", + ValidateFunc: validation.StringInSlice([]string{"exclusively", "yes", "no"}, 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_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) +} 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) +} 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_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) +} diff --git a/routeros/resource_ip_ipsec_policy_test.go b/routeros/resource_ip_ipsec_policy_test.go new file mode 100644 index 00000000..c533f1d6 --- /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-p"), + 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-p`, + 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-p" +} + +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) +} 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) +} 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) +} 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) +} 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) diff --git a/tools/boilerplate/main.go b/tools/boilerplate/main.go index deb6722e..13c67227 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) } @@ -186,7 +201,9 @@ func main() { 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 resource using one of its attributes +terraform import {{.ResourceName}}.test "name=xxx"` var exampleResourceFile = ` resource "{{.ResourceName}}" "test" { @@ -217,19 +234,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 +254,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}}) } ` @@ -274,6 +291,7 @@ func {{.GoResourceName}}() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportStateCustomContext(resSchema), }, Schema: resSchema,