diff --git a/examples/resources/routeros_move_items/resource.tf b/examples/resources/routeros_move_items/resource.tf new file mode 100644 index 00000000..cec83beb --- /dev/null +++ b/examples/resources/routeros_move_items/resource.tf @@ -0,0 +1,79 @@ +variable "rule" { + type = list(object({ + chain = string + action = string + connection_state = optional(string) + in_interface_list = optional(string, "all") + out_interface_list = optional(string) + src_address = optional(string, "0.0.0.0/0") + dst_address = optional(string) + src_port = optional(string) + dst_port = optional(string) + protocol = optional(string) + comment = optional(string, "(terraform-defined)") + log = optional(bool, false) + disabled = optional(bool, true) + })) + + default = [ + { chain = "input", action = "accept", comment = "00" }, + { chain = "input", action = "accept", comment = "01" }, + { chain = "input", action = "accept", comment = "02" }, + { chain = "input", action = "accept", comment = "03" }, + { chain = "input", action = "accept", comment = "04" }, + { chain = "input", action = "accept", comment = "05" }, + { chain = "input", action = "accept", comment = "06" }, + { chain = "input", action = "accept", comment = "07" }, + { chain = "input", action = "accept", comment = "08" }, + { chain = "input", action = "accept", comment = "09" }, + { chain = "input", action = "accept", comment = "10" }, + { chain = "input", action = "accept", comment = "11" }, + { chain = "input", action = "accept", comment = "12" }, + { chain = "input", action = "accept", comment = "13" }, + { chain = "input", action = "accept", comment = "14" }, + { chain = "input", action = "accept", comment = "15" }, + { chain = "input", action = "accept", comment = "16" }, + { chain = "input", action = "accept", comment = "17" }, + { chain = "input", action = "accept", comment = "18" }, + { chain = "input", action = "accept", comment = "19" }, + { chain = "input", action = "accept", comment = "20" }, + { chain = "input", action = "accept", comment = "21" }, + { chain = "input", action = "accept", comment = "22" }, + { chain = "input", action = "accept", comment = "23" }, + { chain = "input", action = "accept", comment = "24" }, + { chain = "input", action = "accept", comment = "25" }, + { chain = "input", action = "accept", comment = "26" }, + { chain = "input", action = "accept", comment = "27" }, + { chain = "input", action = "accept", comment = "28" }, + { chain = "input", action = "accept", comment = "29" }, + { chain = "input", action = "accept", comment = "30" }, + { chain = "input", action = "accept", comment = "31" }, + ] +} + +locals { + # https://discuss.hashicorp.com/t/does-map-sort-keys/12056/2 + # Map keys are always iterated in lexicographical order! + rule_map = { for idx, rule in var.rule : format("%03d", idx) => rule } +} + +resource "routeros_ip_firewall_filter" "rules" { + for_each = local.rule_map + chain = each.value.chain + action = each.value.action + comment = each.value.comment + log = each.value.log + disabled = each.value.disabled + connection_state = each.value.connection_state + in_interface_list = each.value.in_interface_list + src_address = each.value.src_address + dst_port = each.value.dst_port + protocol = each.value.protocol +} + +resource "routeros_move_items" "fw_rules" { + # resource_name = "routeros_ip_firewall_filter" + resource_path = "/ip/firewall/filter" + sequence = [for i, _ in local.rule_map : routeros_ip_firewall_filter.rules[i].id] + depends_on = [routeros_ip_firewall_filter.rules] +} diff --git a/routeros/mikrotik_client.go b/routeros/mikrotik_client.go index e95cc89a..858d58b5 100644 --- a/routeros/mikrotik_client.go +++ b/routeros/mikrotik_client.go @@ -33,6 +33,7 @@ const ( crudSign crudRemove crudRevoke + crudMove ) func NewClient(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { diff --git a/routeros/mikrotik_client_api.go b/routeros/mikrotik_client_api.go index 4c7ae5a2..8934c70b 100644 --- a/routeros/mikrotik_client_api.go +++ b/routeros/mikrotik_client_api.go @@ -28,6 +28,7 @@ var ( crudSign: "/sign", crudRemove: "/remove", crudRevoke: "/issued-revoke", + crudMove: "/move", } ) diff --git a/routeros/mikrotik_client_rest.go b/routeros/mikrotik_client_rest.go index 01af5820..bfa677e5 100644 --- a/routeros/mikrotik_client_rest.go +++ b/routeros/mikrotik_client_rest.go @@ -35,6 +35,7 @@ var ( crudSign: "POST", crudRemove: "POST", crudRevoke: "POST", + crudMove: "POST", } ) diff --git a/routeros/provider.go b/routeros/provider.go index ac16b5f6..a625639e 100644 --- a/routeros/provider.go +++ b/routeros/provider.go @@ -202,6 +202,8 @@ func Provider() *schema.Provider { "routeros_user_manager_user": ResourceUserManagerUser(), "routeros_user_manager_user_group": ResourceUserManagerUserGroup(), "routeros_user_manager_user_profile": ResourceUserManagerUserProfile(), + + "routeros_move_items": ResourceMoveItems(), }, DataSourcesMap: map[string]*schema.Resource{ "routeros_firewall": DatasourceFirewall(), diff --git a/routeros/resource_move.go b/routeros/resource_move.go new file mode 100644 index 00000000..cdadede1 --- /dev/null +++ b/routeros/resource_move.go @@ -0,0 +1,121 @@ +package routeros + +import ( + "context" + "fmt" + "regexp" + "strings" + + "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" +) + +func ResourceMoveItems() *schema.Resource { + resSchema := map[string]*schema.Schema{ + MetaResourcePath: PropResourcePath("/path"), + MetaId: PropId(Id), + + "resource_name": { + Type: schema.TypeString, + Optional: true, + Description: "Resource name in the notation ```routeros_ip_firewall_filter```.", + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^routeros(_\w+)+$`), ""), + AtLeastOneOf: []string{"resource_name", "resource_path"}, + }, + "resource_path": { + Type: schema.TypeString, + Optional: true, + Description: "URL path of the resource in the notation ```/ip/firewall/filter```.", + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^(/\w+)+$`), ""), + AtLeastOneOf: []string{"resource_name", "resource_path"}, + }, + "sequence": { + Type: schema.TypeList, + Required: true, + Description: "List identifiers in the required sequence.", + Elem: &schema.Schema{ + Type: schema.TypeString, + MinItems: 2, + }, + }, + } + resRead := func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + path, ok := d.GetOk("resource_path") + if !ok { + path = d.Get("resource_name") + path = strings.TrimPrefix(path.(string), "routeros_") + path = strings.ReplaceAll(path.(string), "_", "/") + } + + res, err := ReadItems(nil, path.(string), m.(Client)) + if err != nil { + ColorizedDebug(ctx, fmt.Sprintf(ErrorMsgGet, err)) + return diag.FromErr(err) + } + + // Resource not found. + if len(*res) == 0 { + d.SetId("") + return nil + } + + var conf = make(map[string]struct{}) + for _, v := range d.Get("sequence").([]any) { + conf[v.(string)] = struct{}{} + } + + var list []string + for _, r := range *res { + if id, ok := r[".id"]; ok { + if _, ok := conf[id]; ok { + list = append(list, id) + } + } + } + + d.Set("sequence", list) + return nil + } + + resCreateUpdate := func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var list []string + for _, v := range d.Get("sequence").([]any) { + list = append(list, v.(string)) + } + + item := MikrotikItem{ + "numbers": strings.Join(list[:len(list)-1], ","), + "destination": list[len(list)-1], + } + + path, ok := d.GetOk("resource_path") + if !ok { + path = d.Get("resource_name") + path = strings.TrimPrefix(path.(string), "routeros_") + path = strings.ReplaceAll(path.(string), "_", "/") + } + + if m.(Client).GetTransport() == TransportREST { + path = path.(string) + "/move" + } + err := m.(Client).SendRequest(crudMove, &URL{Path: path.(string)}, item, nil) + if err != nil { + ColorizedDebug(ctx, fmt.Sprintf(ErrorMsgPut, err)) + return diag.FromErr(err) + } + + d.SetId(strings.ReplaceAll(strings.TrimLeft(path.(string), "/"), "/", ".")) + + return resRead(ctx, d, m) + } + + return &schema.Resource{ + CreateContext: resCreateUpdate, + ReadContext: resRead, + UpdateContext: resCreateUpdate, + DeleteContext: DefaultSystemDelete(resSchema), + + Schema: resSchema, + } +}