Skip to content

Commit

Permalink
Merge pull request #205 from terraform-routeros/next
Browse files Browse the repository at this point in the history
BGP, veth, bonding support
  • Loading branch information
vaerh committed May 14, 2023
2 parents 3fa94fd + a7de21f commit 7a15938
Show file tree
Hide file tree
Showing 28 changed files with 1,749 additions and 64 deletions.
1 change: 1 addition & 0 deletions .github/workflows/module_testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ jobs:
- /dev/net/tun:/dev/net/tun
options: >-
--cap-add=NET_ADMIN
--entrypoint /routeros/entrypoint_with_four_interfaces.sh
3 changes: 3 additions & 0 deletions examples/resources/routeros_bgp_connection/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#The ID can be found via API or the terminal
#The command for the terminal is -> :put [/routing/bgp/connection get [print show-ids]]
terraform import routeros_bgp_connection.test *3
13 changes: 13 additions & 0 deletions examples/resources/routeros_bgp_connection/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
resource "routeros_bgp_connection" "test" {
name = "neighbor-test"
as = "65550/5"
as_override = true
add_path_out = "none"
remote {
address = "172.17.0.1"
as = "12345/5"
}
local {
role = "ebgp"
}
}
3 changes: 3 additions & 0 deletions examples/resources/routeros_bgp_template/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#The ID can be found via API or the terminal
#The command for the terminal is -> :put [/routing/bgp/template get [print show-ids]]
terraform import routeros_bgp_template.test *3
14 changes: 14 additions & 0 deletions examples/resources/routeros_bgp_template/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
resource "routeros_bgp_template" "test" {
name = "test-template"
as = 65521
input {
limit_process_routes_ipv4 = 5
limit_process_routes_ipv6 = 5
}
output {
affinity = "alone"
keep_sent_attributes = true
default_originate = "never"
}
// save_to = "bgp.dump"
}
3 changes: 3 additions & 0 deletions examples/resources/routeros_interface_bonding/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#The ID can be found via API or the terminal
#The command for the terminal is -> :put [/interface/bonding get [print show-ids]]
terraform import routeros_interface_bonding.test "*0"
4 changes: 4 additions & 0 deletions examples/resources/routeros_interface_bonding/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "routeros_interface_bonding" "test" {
name = "bonding-test"
slaves = ["ether3", "ether4"]
}
3 changes: 3 additions & 0 deletions examples/resources/routeros_interface_veth/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#The ID can be found via API or the terminal
#The command for the terminal is -> :put [/interface/veth get [print show-ids]]
terraform import routeros_interface_veth.test "*0"
6 changes: 6 additions & 0 deletions examples/resources/routeros_interface_veth/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resource "routeros_interface_veth" "test" {
name = "veth-test"
address = "192.168.120.2/24"
gateway = "192.168.120.1"
comment = "Virtual interface"
}
22 changes: 22 additions & 0 deletions routeros/mikrotik.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,25 @@ func BoolFromMikrotikJSON(s string) bool {
}
return false
}

// Map helpers.

func BoolToMikrotikJSONStr(s string) string {
if s == "true" {
return "yes"
}
if s == "false" {
return "no"
}
return s
}

func BoolFromMikrotikJSONStr(s string) string {
if s == "yes" {
return "true"
}
if s == "no" {
return "false"
}
return s
}
144 changes: 111 additions & 33 deletions routeros/mikrotik_serialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"
"time"

"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
Expand Down Expand Up @@ -38,29 +39,34 @@ func GetMetadata(s map[string]*schema.Schema) *MikrotikItemMetadata {
return meta
}

func isEmpty(propName string, s map[string]*schema.Schema, d *schema.ResourceData) bool {
func isEmpty(propName string, schemaProp *schema.Schema, d *schema.ResourceData, confValue cty.Value) bool {
v := d.Get(propName)
switch s[propName].Type {
switch schemaProp.Type {
case schema.TypeString:
if s[propName].Default != nil {
return v.(string) == "" && s[propName].Default.(string) == ""
if schemaProp.Default != nil {
return v.(string) == "" && schemaProp.Default.(string) == ""
}
return v.(string) == ""
return v.(string) == "" && confValue.IsNull()
case schema.TypeInt:
return !d.HasChange(propName)
case schema.TypeBool:
if s[propName].Default != nil {
return s[propName].Default.(bool) == v.(bool)
// If true, it is always not empty:
if v.(bool) {
return false
}
return v.(bool)
// Use the default value:
if schemaProp.Default != nil {
return false
}
return confValue.IsNull()
case schema.TypeList:
return len(v.([]interface{})) == 0
case schema.TypeSet:
return v.(*schema.Set).Len() == 0
case schema.TypeMap:
return len(v.(map[string]interface{})) == 0
default:
panic("[isEmpty] wrong resource type: " + s[propName].Type.String())
panic("[isEmpty] wrong resource type: " + schemaProp.Type.String())
}
}

Expand Down Expand Up @@ -107,6 +113,7 @@ func ListToString(v any) (res string) {
func TerraformResourceDataToMikrotik(s map[string]*schema.Schema, d *schema.ResourceData) (MikrotikItem, *MikrotikItemMetadata) {
item := MikrotikItem{}
meta := &MikrotikItemMetadata{}
rawConfig := d.GetRawConfig()
var transformSet map[string]string
var skipFields map[string]struct{}

Expand Down Expand Up @@ -164,9 +171,18 @@ func TerraformResourceDataToMikrotik(s map[string]*schema.Schema, d *schema.Reso
# (22 unchanged attributes hidden)
}
*/
if terraformMetadata.Optional && !d.HasChange(terraformSnakeName) && isEmpty(terraformSnakeName, s, d) {
/*
old, new := d.GetChange(terraformSnakeName)
conf := d.GetRawConfig().GetAttr(terraformSnakeName).IsNull()
fmt.Println(rawConfig.GetAttr(terraformSnakeName).IsKnown())
fmt.Printf("%25s - old: '%10v', new: '%10v', isNull: %v", terraformSnakeName, old, new, conf)
*/
if terraformMetadata.Optional && !d.HasChange(terraformSnakeName) &&
isEmpty(terraformSnakeName, s[terraformSnakeName], d, rawConfig.GetAttr(terraformSnakeName)) {
// fmt.Println(" ... skipped")
continue
}
// fmt.Println()

// terraformSnakeName = fast_forward, schemaPropData = true
// NewMikrotikItem.Fields["fast-forward"] = "true"
Expand All @@ -182,8 +198,39 @@ func TerraformResourceDataToMikrotik(s map[string]*schema.Schema, d *schema.Reso
item[mikrotikKebabName] = BoolToMikrotikJSON(value.(bool))
// Used to represent an ordered collection of items.
case schema.TypeList:
item[mikrotikKebabName] = ListToString(value)
// Used to represent an unordered collection of items.

switch terraformMetadata.Elem.(type) {
case *schema.Schema:

item[mikrotikKebabName] = ListToString(value)

case *schema.Resource:

list := value.([]interface{})[0].(map[string]interface{})
ctyList := rawConfig.GetAttr(terraformSnakeName).AsValueSlice()[0]

for fieldName, value := range list {
// "output.0.affinity"
filedNameInState := fmt.Sprintf("%v.%v.%v", terraformSnakeName, 0, fieldName)
fieldSchema := terraformMetadata.Elem.(*schema.Resource).Schema[fieldName]

if fieldSchema.Optional && !d.HasChange(filedNameInState) &&
isEmpty(filedNameInState, fieldSchema, d, ctyList.GetAttr(fieldName)) {
continue
}
fieldName = SnakeToKebab(mikrotikKebabName + "." + fieldName)

switch value := value.(type) {
case string:
item[fieldName] = value
case int:
item[fieldName] = strconv.Itoa(value)
case bool:
item[fieldName] = BoolToMikrotikJSON(value)
}
}
}
// Used to represent an unordered collection of items.
case schema.TypeSet:
item[mikrotikKebabName] = ListToString(value.(*schema.Set).List())
case schema.TypeMap:
Expand All @@ -197,13 +244,8 @@ func TerraformResourceDataToMikrotik(s map[string]*schema.Schema, d *schema.Reso
}
}

s := v.(string)
// Conversion of boolean values.
if s == "true" {
s = "yes"
} else if s == "false" {
s = "no"
}
s := BoolToMikrotikJSONStr(v.(string))

item[k] = s
}
Expand All @@ -228,9 +270,9 @@ func MikrotikResourceDataToTerraform(item MikrotikItem, s map[string]*schema.Sch
transformSet = loadTransformSet(ts.Default.(string), false)
}

// TypeMaps initialization information storage.
// map["channel"] = bool
var maps = make(map[string]bool)
// TypeMap,TypeSet initialization information storage.
var maps = make(map[string]map[string]interface{})
var nestedLists = make(map[string]map[string]interface{})

// Incoming map iteration.
for mikrotikKebabName, mikrotikValue := range item {
Expand Down Expand Up @@ -307,7 +349,9 @@ func MikrotikResourceDataToTerraform(item MikrotikItem, s map[string]*schema.Sch
// | id = "*2"
// | # (7 unchanged attributes hidden)
// | }
if mikrotikValue != "" {

// Flat Lists & Sets:
if _, ok := s[terraformSnakeName].Elem.(*schema.Schema); mikrotikValue != "" && ok {
for _, v := range strings.Split(mikrotikValue, ",") {
if s[terraformSnakeName].Elem.(*schema.Schema).Type == schema.TypeInt {
i, err := strconv.Atoi(v)
Expand All @@ -328,27 +372,48 @@ func MikrotikResourceDataToTerraform(item MikrotikItem, s map[string]*schema.Sch
}

if s[terraformSnakeName].Type == schema.TypeList {
err = d.Set(terraformSnakeName, l)
switch s[terraformSnakeName].Elem.(type) {
case *schema.Schema:
err = d.Set(terraformSnakeName, l)
case *schema.Resource:
var v any

switch s[terraformSnakeName].Elem.(*schema.Resource).Schema[subFieldSnakeName].Type {
case schema.TypeString:
v = mikrotikValue
case schema.TypeInt:
v, err = strconv.Atoi(mikrotikValue)
if err != nil {
diags = diag.Errorf("%v for '%v.%v' field", err, terraformSnakeName, subFieldSnakeName)
}
case schema.TypeBool:
v = BoolFromMikrotikJSON(mikrotikValue)
}

if err != nil {
break
}

if list, ok := nestedLists[terraformSnakeName]; !ok {
nestedLists[terraformSnakeName] = map[string]interface{}{subFieldSnakeName: v}
} else {
list[subFieldSnakeName] = v
}
}
} else {
err = d.Set(terraformSnakeName,
schema.NewSet(schema.HashSchema(s[terraformSnakeName].Elem.(*schema.Schema)), l))
}

case schema.TypeMap:
if mikrotikValue == "yes" {
mikrotikValue = "true"
} else if mikrotikValue == "no" {
mikrotikValue = "false"
}
// "yes" -> "true"; "no" -> "false"
mikrotikValue = BoolFromMikrotikJSONStr(mikrotikValue)

if _, ok := maps[terraformSnakeName]; !ok {
if m, ok := maps[terraformSnakeName]; !ok {
// Create a new map when processing the first incoming field.
maps[terraformSnakeName] = true
d.Set(terraformSnakeName, map[string]interface{}{subFieldSnakeName: mikrotikValue})
maps[terraformSnakeName] = map[string]interface{}{subFieldSnakeName: mikrotikValue}
} else {
m := d.Get(terraformSnakeName).(map[string]interface{})
m[subFieldSnakeName] = mikrotikValue
d.Set(terraformSnakeName, m)
}

default:
Expand All @@ -368,6 +433,19 @@ func MikrotikResourceDataToTerraform(item MikrotikItem, s map[string]*schema.Sch
}
}

// Lists processing.
for name, list := range nestedLists {
if err = d.Set(name, []interface{}{list}); err != nil {
diags = append(diags, diag.FromErr(err)...)
}
}
// Maps processing.
for name, m := range maps {
if err = d.Set(name, m); err != nil {
diags = append(diags, diag.FromErr(err)...)
}
}

return diags
}

Expand Down
8 changes: 6 additions & 2 deletions routeros/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func Provider() *schema.Provider {
"routeros_interface_list": ResourceInterfaceList(),
"routeros_interface_list_member": ResourceInterfaceListMember(),
"routeros_interface_ovpn_server": ResourceInterfaceOpenVPNServer(),
"routeros_interface_veth": ResourceInterfaceVeth(),
"routeros_interface_bonding": ResourceInterfaceBonding(),

// Aliases for interface objects to retain compatibility between original and fork
"routeros_bridge": ResourceInterfaceBridge(),
Expand Down Expand Up @@ -143,8 +145,10 @@ func Provider() *schema.Provider {
"routeros_capsman_rates": ResourceCapsManRates(),
"routeros_capsman_security": ResourceCapsManSecurity(),

// Routing tables
"routeros_routing_table": ResourceRoutingTable(),
// Routing
"routeros_routing_table": ResourceRoutingTable(),
"routeros_bgp_connection": ResourceBGPConnection(),
"routeros_bgp_template": ResourceBGPTemplate(),

// VPN
"routeros_ovpn_server": ResourceOpenVPNServer(),
Expand Down
7 changes: 7 additions & 0 deletions routeros/provider_schema_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
KeyInterface = "interface"
KeyInvalid = "invalid"
KeyL2Mtu = "l2mtu"
KeyMacAddress = "mac_address"
KeyMtu = "mtu"
KeyName = "name"
KeyPlaceBefore = "place_before"
Expand Down Expand Up @@ -129,6 +130,7 @@ var (
PropDisabledRw = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
}
PropDynamicRo = &schema.Schema{
Type: schema.TypeBool,
Expand Down Expand Up @@ -156,6 +158,11 @@ var (
Computed: true,
Description: "Layer2 Maximum transmission unit.",
}
PropMacAddressRo = &schema.Schema{
Type: schema.TypeString,
Computed: true,
Description: "Current mac address.",
}
PropNameForceNewRw = &schema.Schema{
Type: schema.TypeString,
Required: true,
Expand Down
Loading

0 comments on commit 7a15938

Please sign in to comment.