diff --git a/examples/resources/routeros_interface_ethernet/import.sh b/examples/resources/routeros_interface_ethernet/import.sh new file mode 100644 index 00000000..cb69de42 --- /dev/null +++ b/examples/resources/routeros_interface_ethernet/import.sh @@ -0,0 +1,3 @@ +#The ID can be found via API or the terminal +#The command for the terminal is -> :put [/interface/ethernet get [print show-ids]] +terraform import routeros_interface_ethernet.test "*0" diff --git a/examples/resources/routeros_interface_ethernet/resource.tf b/examples/resources/routeros_interface_ethernet/resource.tf new file mode 100644 index 00000000..8027cf3e --- /dev/null +++ b/examples/resources/routeros_interface_ethernet/resource.tf @@ -0,0 +1,5 @@ +resource "routeros_interface_ethernet" "test" { + factory_name = "sfp-sfpplus8" + name = "swtich-eth0" + mtu = 9000 +} diff --git a/routeros/mikrotik_serialize.go b/routeros/mikrotik_serialize.go index a939c8f1..8bbb3086 100644 --- a/routeros/mikrotik_serialize.go +++ b/routeros/mikrotik_serialize.go @@ -269,12 +269,18 @@ func MikrotikResourceDataToTerraform(item MikrotikItem, s map[string]*schema.Sch var diags diag.Diagnostics var err error var transformSet map[string]string + var skipFields map[string]struct{} // {"channel": "channel.config", "mikrotik-field-name": "schema-field-name"} if ts, ok := s[MetaTransformSet]; ok { transformSet = loadTransformSet(ts.Default.(string), false) } + // "field_first", "field_second", "field_third" + if sf, ok := s[MetaSkipFields]; ok { + skipFields = loadSkipFields(sf.Default.(string)) + } + // TypeMap,TypeSet initialization information storage. var maps = make(map[string]map[string]interface{}) var nestedLists = make(map[string]map[string]interface{}) @@ -305,6 +311,12 @@ func MikrotikResourceDataToTerraform(item MikrotikItem, s map[string]*schema.Sch // field-name => field_name terraformSnakeName := KebabToSnake(mikrotikKebabName) + if skipFields != nil { + if _, ok := skipFields[terraformSnakeName]; ok { + continue + } + } + // Composite fields. var subFieldSnakeName string if strings.Contains(terraformSnakeName, ".") { diff --git a/routeros/resource_default_actions.go b/routeros/resource_default_actions.go index 7e8c9f9a..1a2368fd 100644 --- a/routeros/resource_default_actions.go +++ b/routeros/resource_default_actions.go @@ -174,7 +174,7 @@ func SystemResourceRead(ctx context.Context, s map[string]*schema.Schema, d *sch return diag.FromErr(err) } - // We make a unique Id, it does not affect the work with the Mikrotik. + // We make a unique Id, it does not affect the work with the Mikrotik. // Id: /caps-man/manager -> caps-man.manager d.SetId(strings.ReplaceAll(strings.TrimLeft(metadata.Path, "/"), "/", ".")) diff --git a/routeros/resource_interface_ethernet.go b/routeros/resource_interface_ethernet.go index 3e726e9d..b039e368 100644 --- a/routeros/resource_interface_ethernet.go +++ b/routeros/resource_interface_ethernet.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -44,7 +45,10 @@ import ( */ const poeOutField = "poe_out" +const cableSettingsField = "cable_settings" +const runningCheckField = "disable_running_check" +// ResourceInterfaceEthernet is the schema for ethernet interfaces // https://help.mikrotik.com/docs/display/ROS/Ethernet#Ethernet-Properties func ResourceInterfaceEthernet() *schema.Resource { resSchema := map[string]*schema.Schema{ @@ -62,6 +66,7 @@ func ResourceInterfaceEthernet() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{ "10M-full", "10M-half", "100M-full", "100M-half", "1000M-full", "1000M-half", "2500M-full", "5000M-full", "10000M-full"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, }, KeyArp: PropArpRw, KeyArpTimeout: PropArpTimeoutRw, @@ -74,26 +79,27 @@ func ResourceInterfaceEthernet() *schema.Resource { Note2: Gigabit Ethernet and NBASE-T Ethernet links cannot work with auto-negotiation disabled.`, }, "bandwidth": { - Type: schema.TypeInt, + Type: schema.TypeString, 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.`, + DiffSuppressFunc: AlwaysPresentNotUserProvided, }, "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), + Type: schema.TypeString, + Optional: true, + Description: `Changes the cable length setting (only applicable to NS DP83815/6 cards)`, + ValidateFunc: validation.StringInSlice([]string{"default", "short", "standard"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, }, "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), + ValidateFunc: validation.StringInSlice([]string{"auto", "copper", "sfp"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, }, KeyComment: PropCommentRw, "default_name": { @@ -109,6 +115,26 @@ func ResourceInterfaceEthernet() *schema.Resource { Default: true, Optional: true, }, + "driver_rx_byte": { + Type: schema.TypeInt, + Computed: true, + Description: `Total count of received bytes on device CPU`, + }, + "driver_rx_packet": { + Type: schema.TypeInt, + Computed: true, + Description: `Total count of received packets on device CPU`, + }, + "driver_tx_byte": { + Type: schema.TypeInt, + Computed: true, + Description: `Total count of transmitted packets by device CPU`, + }, + "driver_tx_packet": { + Type: schema.TypeInt, + Computed: true, + Description: `Total count of transmitted packets by device CPU`, + }, "factory_name": { Type: schema.TypeString, Optional: false, @@ -121,7 +147,12 @@ func ResourceInterfaceEthernet() *schema.Resource { Default: true, Optional: true, }, - KeyL2Mtu: PropL2MtuRo, + KeyL2Mtu: { + Type: schema.TypeInt, + Optional: true, + Description: "Layer2 Maximum transmission unit. " + + "[See](https://wiki.mikrotik.com/wiki/Maximum_Transmission_Unit_on_RouterBoards).", + }, "loop_protect": { Type: schema.TypeString, Optional: true, @@ -147,10 +178,10 @@ func ResourceInterfaceEthernet() *schema.Resource { Computed: true, }, "mac_address": { - Type: schema.TypeString, - Description: `Media Access Control number of an interface.`, - Optional: true, - Computed: true, + Type: schema.TypeString, + Description: `Media Access Control number of an interface.`, + Optional: true, + DiffSuppressFunc: AlwaysPresentNotUserProvided, }, "mdix_enable": { Type: schema.TypeBool, @@ -172,11 +203,12 @@ func ResourceInterfaceEthernet() *schema.Resource { 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), + 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), + DiffSuppressFunc: AlwaysPresentNotUserProvided, }, "poe_priority": { Type: schema.TypeInt, @@ -199,6 +231,26 @@ func ResourceInterfaceEthernet() *schema.Resource { Computed: true, Description: "Total count of received bytes.", }, + "rx_error_events": { + Type: schema.TypeInt, + Computed: true, + Description: `Total count of received frames with active error event`, + }, + "rx_fcs_error": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of received frames with incorrect checksum", + }, + "rx_fragment": { + Type: schema.TypeInt, + Computed: true, + Description: `Total count of received fragmented frames (not related to IP fragmentation)`, + }, + "rx_jabber": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of received jabbed packets - a packet that is transmitted longer than the maximum packet length", + }, "rx_multicast": { Type: schema.TypeInt, Computed: true, @@ -209,13 +261,46 @@ func ResourceInterfaceEthernet() *schema.Resource { Computed: true, Description: "Total count of received packets.", }, + "rx_pause": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of received pause frames", + }, "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", + Default: "off", + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"on", "off", "auto"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, + }, + "rx_overflow": { + Type: schema.TypeInt, + Description: `Total count of received overflowed frames, can be caused when device resources are insufficient to receive a certain frame`, + Computed: true, + }, + "rx_too_long": { + Type: schema.TypeInt, + Description: `Total count of received frames that were larger than the maximum supported frame size by the network device, see the max-l2mtu property`, + Computed: true, + }, + "rx_too_short": { + Type: schema.TypeInt, + Computed: true, + Description: `Total count of received frame shorter than the minimum 64 bytes`, + }, + "rx_unicast": { + Type: schema.TypeInt, + Computed: true, + Description: `Total count of received unicast frames`, + }, + "sfp_rate_select": { + Type: schema.TypeString, Optional: true, - ValidateFunc: validation.StringInSlice([]string{"on", "off", "auto"}, false), + Description: `Allows to control rate select pin for SFP ports. Values: high | low`, + Default: "high", + ValidateFunc: validation.StringInSlice([]string{"high", "low"}, false), }, "sfp_shutdown_temperature": { Type: schema.TypeInt, @@ -235,7 +320,7 @@ func ResourceInterfaceEthernet() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{"10Mbps", "10Gbps", "100Mbps", "1Gbps"}, false), }, "switch": { - Type: schema.TypeInt, + Type: schema.TypeString, Description: "ID to which switch chip interface belongs to.", Computed: true, }, @@ -244,9 +329,10 @@ func ResourceInterfaceEthernet() *schema.Resource { 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), + Default: "off", + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"on", "off", "auto"}, false), + DiffSuppressFunc: AlwaysPresentNotUserProvided, }, "tx_broadcast": { Type: schema.TypeInt, @@ -263,18 +349,79 @@ func ResourceInterfaceEthernet() *schema.Resource { Computed: true, Description: "Total count of transmitted multicast frames.", }, + "tx_collision": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted frames that made collisions", + }, + "tx_drop": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted frames that were dropped due to already full output queue", + }, + + "tx_late_collision": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted frames that made collision after being already halfway transmitted", + }, "tx_packet": { Type: schema.TypeInt, Computed: true, Description: "Total count of transmitted packets.", }, + "tx_pause": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted pause frames.", + }, + "tx_rx_64": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted and received 64 byte frames", + }, + "tx_rx_65_127": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted and received 64 to 127 byte frames", + }, + "tx_rx_128_255": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted and received 128 to 255 byte frames", + }, + "tx_rx_256_511": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted and received 256 to 511 byte frames", + }, + "tx_rx_512_1023": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted and received 512 to 1024 byte frames", + }, + "tx_rx_1024_max": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted and received 1024 or above byte frames", + }, + "tx_underrun": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted underrun packets", + }, + "tx_unicast": { + Type: schema.TypeInt, + Computed: true, + Description: "Total count of transmitted unicast frames.", + }, } return &schema.Resource{ - CreateContext: UpdateOnlyDeviceCreate(resSchema), - ReadContext: DefaultRead(resSchema), - UpdateContext: DefaultUpdate(resSchema), - DeleteContext: NoOpDelete, + CreateContext: updateOnlyDeviceCreate(resSchema), + ReadContext: updateOnlyDeviceRead(resSchema), + UpdateContext: updateOnlyDeviceUpdate(resSchema), + DeleteContext: DefaultSystemDelete(resSchema), Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -284,36 +431,77 @@ func ResourceInterfaceEthernet() *schema.Resource { } } -func UpdateOnlyDeviceCreate(s map[string]*schema.Schema) schema.CreateContextFunc { +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) - } + return updateEthernetInterface(ctx, s, d, m) + } +} - // 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) - } +func updateOnlyDeviceUpdate(s map[string]*schema.Schema) schema.UpdateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return updateEthernetInterface(ctx, s, d, m) + } +} - d.SetId(ethernetInterface.GetID(Id)) - if updateDiag := ResourceUpdate(ctx, s, d, m); updateDiag.HasError() { - return updateDiag - } +func updateOnlyDeviceRead(s map[string]*schema.Schema) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return readEthernetInterface(ctx, s, d, m) + } +} - return ResourceRead(ctx, s, d, m) +func readEthernetInterface(ctx context.Context, s map[string]*schema.Schema, d *schema.ResourceData, m interface{}) diag.Diagnostics { + ethernetInterface, err := findInterfaceByDefaultName(s, d, m.(Client)) + if err != nil { + return diag.FromErr(err) } + s = updateSchemaWithRouterCapabilities(s, ethernetInterface) + return DefaultRead(s)(ctx, d, m) } -func NoOpDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - return nil +// updateEthernetInterface searches for the interface and disables fields not supported by the router instance +func updateEthernetInterface(ctx context.Context, s map[string]*schema.Schema, 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, lets 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 = skipFieldInSchema(s[MetaSkipFields].Default, poeOutField) + } + + if _, supportsCableSettings := ethernetInterface[SnakeToKebab(cableSettingsField)]; !supportsCableSettings { + s[MetaSkipFields].Default = skipFieldInSchema(s[MetaSkipFields].Default, cableSettingsField) + } + + if _, supportsRunningCheck := ethernetInterface[SnakeToKebab(runningCheckField)]; !supportsRunningCheck { + s[MetaSkipFields].Default = skipFieldInSchema(s[MetaSkipFields].Default, runningCheckField) + } + + d.SetId(ethernetInterface.GetID(Id)) + if updateDiag := ResourceUpdate(ctx, s, d, m); updateDiag.HasError() { + return updateDiag + } + + return readEthernetInterface(ctx, s, d, m) +} + +func updateSchemaWithRouterCapabilities(s map[string]*schema.Schema, item MikrotikItem) map[string]*schema.Schema { + // Dynamic schema, counters for tx_queue${number}_packets, changes from router to router, read only counters. + // Just drop them as they don't have much sense in the context of a terraform provider + for key := range item { + if strings.HasPrefix(key, "tx-queue") { + s[MetaSkipFields].Default = skipFieldInSchema(s[MetaSkipFields].Default, KebabToSnake(key)) + } + } + return s } func findInterfaceByDefaultName(s map[string]*schema.Schema, d *schema.ResourceData, c Client) (MikrotikItem, error) { @@ -331,3 +519,7 @@ func findInterfaceByDefaultName(s map[string]*schema.Schema, d *schema.ResourceD ethernetInterface := (*items)[0] return ethernetInterface, nil } + +func skipFieldInSchema(defaults interface{}, field string) string { + return fmt.Sprintf("%s,\"%s\"", defaults, field) +}