Skip to content

Commit

Permalink
feat: Implementation of routeos_interface_ethernet (#256)(#255)
Browse files Browse the repository at this point in the history
feat: Implementation of routeros_interface_ethernet 

* Partial implementation of interface_ethernet

* Add additional properties and readonly properties

* Generate docs

* Fixes for the resource, add test skeleton

* Working implementation

* Revert changes in documentation

* fix: Sort attributes

* fix: Add lost attributes

* fix: Fix attributes

* Add tests validation, ensure read after write so all access methods (API/Rest) get the running values.

* Fix merge issues (broken tests)

* Add sample rest response

---------

Co-authored-by: Vaerh <[email protected]>
  • Loading branch information
jlpedrosa and vaerh authored Sep 19, 2023
1 parent bedf4e4 commit 0d848bf
Show file tree
Hide file tree
Showing 4 changed files with 430 additions and 7 deletions.
1 change: 1 addition & 0 deletions routeros/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func Provider() *schema.Provider {
"routeros_interface_veth": ResourceInterfaceVeth(),
"routeros_interface_bonding": ResourceInterfaceBonding(),
"routeros_interface_pppoe_client": ResourceInterfacePPPoEClient(),
"routeros_interface_ethernet": ResourceInterfaceEthernet(),

// Aliases for interface objects to retain compatibility between original and fork
"routeros_bridge": ResourceInterfaceBridge(),
Expand Down
20 changes: 13 additions & 7 deletions routeros/provider_schema_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,15 @@ var (
Computed: true,
}
PropArpRw = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "enabled",
Description: "ARP resolution protocol mode.",
Type: schema.TypeString,
Optional: true,
Default: "enabled",
Description: `Address Resolution Protocol mode:
disabled - the interface will not use ARP
enabled - the interface will use ARP
local-proxy-arp - the router performs proxy ARP on the interface and sends replies to the same interface
proxy-arp - the router performs proxy ARP on the interface and sends replies to other interfaces
reply-only - the interface will only reply to requests originated from matching IP address/MAC address combinations which are entered as static entries in the ARP table. No dynamic entries will be automatically stored in the ARP table. Therefore for communications to be successful, a valid static entry must already exist.`,
ValidateFunc: validation.StringInSlice([]string{"disabled", "enabled", "local-proxy-arp", "proxy-arp",
"reply-only"}, false),
}
Expand Down Expand Up @@ -160,9 +165,10 @@ var (
Computed: true,
}
PropL2MtuRo = &schema.Schema{
Type: schema.TypeInt,
Computed: true,
Description: "Layer2 Maximum transmission unit.",
Type: schema.TypeInt,
Computed: true,
Description: "Layer2 Maximum transmission unit. " +
"[See](https://wiki.mikrotik.com/wiki/Maximum_Transmission_Unit_on_RouterBoards).",
}
PropMacAddressRo = &schema.Schema{
Type: schema.TypeString,
Expand Down
333 changes: 333 additions & 0 deletions routeros/resource_interface_ethernet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
package routeros

import (
"context"
"errors"
"fmt"

"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",
"advertise": "10M-half,10M-full,100M-half,100M-full,1000M-full",
"arp": "enabled",
"arp-timeout": "auto",
"auto-negotiation": "true",
"cable-settings": "default",
"default-name": "ether1",
"disable-running-check": "true",
"disabled": "false",
"loop-protect": "default",
"loop-protect-disable-time": "5m",
"loop-protect-send-interval": "5s",
"loop-protect-status": "off",
"mac-address": "54:05:AB:1E:BE:71",
"mtu": "1500",
"name": "ether1",
"orig-mac-address": "54:05:AB:1E:BE:71",
"running": "true",
"rx-broadcast": "250",
"rx-bytes": "222253",
"rx-flow-control": "off",
"rx-multicast": "10",
"rx-packet": "1889",
"tx-broadcast": "113",
"tx-bytes": "693931",
"tx-flow-control": "off",
"tx-multicast": "270",
"tx-packet": "2222"
}
*/

const poeOutField = "poe_out"

// https://help.mikrotik.com/docs/display/ROS/Ethernet#Ethernet-Properties
func ResourceInterfaceEthernet() *schema.Resource {
resSchema := map[string]*schema.Schema{
MetaResourcePath: PropResourcePath("/interface/ethernet"),
MetaId: PropId(Id),
MetaSkipFields: PropSkipFields(`"factory_name"`),
"advertise": {
Type: schema.TypeString,
Optional: true,
Default: "",
Description: `
Advertised speed and duplex modes for Ethernet interfaces over twisted pair,
only applies when auto-negotiation is enabled. Advertising higher speeds than
the actual interface supported speed will have no effect, multiple options are allowed.`,
ValidateFunc: validation.StringInSlice([]string{
"10M-full", "10M-half", "100M-full", "100M-half",
"1000M-full", "1000M-half", "2500M-full", "5000M-full", "10000M-full"}, false),
},
KeyArp: PropArpRw,
KeyArpTimeout: PropArpTimeoutRw,
"auto_negotiation": {
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: `When enabled, the interface "advertises" its maximum capabilities to achieve the best connection possible.
Note1: Auto-negotiation should not be disabled on one end only, otherwise Ethernet Interfaces may not work properly.
Note2: Gigabit Ethernet and NBASE-T Ethernet links cannot work with auto-negotiation disabled.`,
},
"bandwidth": {
Type: schema.TypeInt,
Optional: true,
Description: `Sets max rx/tx bandwidth in kbps that will be handled by an interface. TX limit is supported on all Atheros switch-chip ports.
RX limit is supported only on Atheros8327/QCA8337 switch-chip ports.`,
},
"cable_settings": {
Type: schema.TypeString,
Optional: true,
Default: "default",
Description: `Changes the cable length setting (only applicable to NS DP83815/6 cards)`,
ValidateFunc: validation.StringInSlice([]string{"default", "short", "standard"}, false),
},
"combo_mode": {
Type: schema.TypeString,
Optional: true,
Default: "auto",
Description: `When auto mode is selected, the port that was first connected will establish the link. In case this link fails, the other port will try to establish a new link. If both ports are connected at the same time (e.g. after reboot),
the priority will be the SFP/SFP+ port. When sfp mode is selected, the interface will only work through SFP/SFP+ cage.
When copper mode is selected, the interface will only work through RJ45 Ethernet port.`,
ValidateFunc: validation.StringInSlice([]string{"auto", "copper", "sfp"}, false),
},
KeyComment: PropCommentRw,
"default_name": {
Type: schema.TypeString,
Computed: true,
Description: "The default name for an interface.",
},
KeyDisabled: PropDisabledRw,
"disable_running_check": {
Type: schema.TypeBool,
Description: `Disable running check. If this value is set to 'no', the router automatically detects whether the NIC is connected with a device in the network or not.
Default value is 'yes' because older NICs do not support it. (only applicable to x86)`,
Default: true,
Optional: true,
},
"factory_name": {
Type: schema.TypeString,
Optional: false,
Required: true,
Description: "The factory name of the identifier, serves as resource identifier. Determines which interface will be updated.",
},
"full_duplex": {
Type: schema.TypeBool,
Description: `Defines whether the transmission of data appears in two directions simultaneously, only applies when auto-negotiation is disabled.`,
Default: true,
Optional: true,
},
KeyL2Mtu: PropL2MtuRo,
"loop_protect": {
Type: schema.TypeString,
Optional: true,
Default: "default",
ValidateFunc: validation.StringInSlice([]string{"default", "on", "off"}, false),
},
"loop_protect_disable_time": {
Type: schema.TypeString,
Optional: true,
Default: "5m",
ValidateFunc: ValidationTime,
DiffSuppressFunc: TimeEquall,
},
"loop_protect_send_interval": {
Type: schema.TypeString,
Optional: true,
Default: "5s",
ValidateFunc: ValidationTime,
DiffSuppressFunc: TimeEquall,
},
"loop_protect_status": {
Type: schema.TypeString,
Computed: true,
},
"mac_address": {
Type: schema.TypeString,
Description: `Media Access Control number of an interface.`,
Optional: true,
Computed: true,
},
"mdix_enable": {
Type: schema.TypeBool,
Description: `Whether the MDI/X auto cross over cable correction feature is enabled for the port (Hardware specific, e.g. ether1 on RB500 can be set to yes/no. Fixed to 'yes' on other hardware.)`,
Optional: true,
Default: true,
},
"mtu": {
Type: schema.TypeInt,
Optional: true,
Default: 1500,
Description: "Layer3 Maximum transmission unit",
ValidateFunc: validation.IntBetween(0, 65536),
},
KeyName: PropName("Name of the ethernet interface."),
"orig_mac_address": {
Type: schema.TypeString,
Description: "Original Media Access Control number of an interface. (read only)",
Computed: true,
},
poeOutField: {
Type: schema.TypeString,
Description: "PoE settings: (https://wiki.mikrotik.com/wiki/Manual:PoE-Out)",
Default: "off",
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"auto-on", "forced-on", "off"}, false),
},
"poe_priority": {
Type: schema.TypeInt,
Description: "PoE settings: (https://wiki.mikrotik.com/wiki/Manual:PoE-Out)",
Optional: true,
ValidateFunc: validation.IntBetween(0, 99),
},
"running": {
Type: schema.TypeBool,
Description: "Whether interface is running. Note that some interface does not have running check and they are always reported as \"running\"",
Computed: true,
},
"rx_broadcast": {
Type: schema.TypeInt,
Computed: true,
Description: "Total count of received broadcast frames.",
},
"rx_bytes": {
Type: schema.TypeInt,
Computed: true,
Description: "Total count of received bytes.",
},
"rx_multicast": {
Type: schema.TypeInt,
Computed: true,
Description: "Total count of received multicast frames.",
},
"rx_packet": {
Type: schema.TypeInt,
Computed: true,
Description: "Total count of received packets.",
},
"rx_flow_control": {
Type: schema.TypeString,
Description: `When set to on, the port will process received pause frames and suspend transmission if required.
auto is the same as on except when auto-negotiation=yes flow control status is resolved by taking into account what other end advertises.`,
Default: "off",
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"on", "off", "auto"}, false),
},
"sfp_shutdown_temperature": {
Type: schema.TypeInt,
Description: "The temperature in Celsius at which the interface will be temporarily turned off due to too high detected SFP module temperature (introduced v6.48)." +
"The default value for SFP/SFP+/SFP28 interfaces is 95, and for QSFP+/QSFP28 interfaces 80 (introduced v7.6).",
Optional: true,
},
"slave": {
Type: schema.TypeBool,
Description: "Whether interface is configured as a slave of another interface (for example Bonding)",
Computed: true,
},
"speed": {
Type: schema.TypeString,
Description: "Sets interface data transmission speed which takes effect only when auto-negotiation is disabled.",
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"10Mbps", "10Gbps", "100Mbps", "1Gbps"}, false),
},
"switch": {
Type: schema.TypeInt,
Description: "ID to which switch chip interface belongs to.",
Computed: true,
},
"tx_flow_control": {
Type: schema.TypeString,
Description: `When set to on, the port will generate pause frames to the upstream device to temporarily stop the packet transmission.
Pause frames are only generated when some routers output interface is congested and packets cannot be transmitted anymore.
Auto is the same as on except when auto-negotiation=yes flow control status is resolved by taking into account what other end advertises.`,
Default: "off",
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"on", "off", "auto"}, false),
},
"tx_broadcast": {
Type: schema.TypeInt,
Computed: true,
Description: "Total count of transmitted broadcast frames.",
},
"tx_bytes": {
Type: schema.TypeInt,
Computed: true,
Description: "Total count of transmitted bytes.",
},
"tx_multicast": {
Type: schema.TypeInt,
Computed: true,
Description: "Total count of transmitted multicast frames.",
},
"tx_packet": {
Type: schema.TypeInt,
Computed: true,
Description: "Total count of transmitted packets.",
},
}

return &schema.Resource{
CreateContext: UpdateOnlyDeviceCreate(resSchema),
ReadContext: DefaultRead(resSchema),
UpdateContext: DefaultUpdate(resSchema),
DeleteContext: NoOpDelete,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: resSchema,
}
}

func UpdateOnlyDeviceCreate(s map[string]*schema.Schema) schema.CreateContextFunc {
return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
ethernetInterface, err := findInterfaceByDefaultName(s, d, m.(Client))
if err != nil {
return diag.FromErr(err)
}

// Router won't accept poe-out parameter if the interface does not support it.
poeDesiredState := d.Get(poeOutField)
_, supportsPoE := ethernetInterface[SnakeToKebab(poeOutField)]
switch {
// if the user has specified it, but it's not supported, let's error out
case poeDesiredState != "off" && !supportsPoE:
return diag.FromErr(errors.New("can't configure PoE, router does not supports it"))
// if the router does not support PoE, avoid sending the parameter as it returns an error.
case !supportsPoE:
s[MetaSkipFields].Default = fmt.Sprintf("%s,\"%s\"", s[MetaSkipFields].Default, poeOutField)
}

d.SetId(ethernetInterface.GetID(Id))
if updateDiag := ResourceUpdate(ctx, s, d, m); updateDiag.HasError() {
return updateDiag
}

return ResourceRead(ctx, s, d, m)
}
}

func NoOpDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
return nil
}

func findInterfaceByDefaultName(s map[string]*schema.Schema, d *schema.ResourceData, c Client) (MikrotikItem, error) {
metadata := GetMetadata(s)
filter := fmt.Sprintf("default-name=%s", d.Get("factory_name"))
items, err := ReadItemsFiltered([]string{filter}, metadata.Path, c)
if err != nil {
return nil, err
}

if items == nil || len(*items) == 0 {
return nil, errors.New("unable to find interface")
}

ethernetInterface := (*items)[0]
return ethernetInterface, nil
}
Loading

0 comments on commit 0d848bf

Please sign in to comment.