yast/yast-yast2

View on GitHub
library/network/src/modules/NetworkInterfaces.rb

Summary

Maintainability
F
1 wk
Test Coverage
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************

require "yast"
require "shellwords"

module Yast
  # Reads and writes the ifcfg files (/etc/sysconfig/network/ifcfg-*).
  # Categorizes the configurations according to type.
  # Presents them one ifcfg at a time through the {#Current} hash.
  class NetworkInterfacesClass < Module
    attr_reader :Devices

    include Logger

    Yast.import "String"

    # A single character used to separate alias id
    ALIAS_SEPARATOR = "#".freeze
    TYPE_REGEX = "(ip6tnl|mip6mnha|[#{String.CAlpha}]+)".freeze
    ID_REGEX = "([^#{ALIAS_SEPARATOR}]*)".freeze
    ALIAS_REGEX = "(.*)".freeze
    DEVNAME_REGEX = "#{TYPE_REGEX}-?#{ID_REGEX}".freeze

    # @attribute Name
    # @return [String]
    #
    # Current device identifier, like eth0, eth1:blah, lo, ...
    #
    # {#Add}, {#Edit} and {#Delete} copy the requested device info
    # (via {#Select}) to {#Name} and {#Current}, {#Commit} puts it back.

    # @attribute Current
    # @return [Hash<String>]
    #
    # Current device information
    # like { "BOOTPROTO"=>"dhcp", "STARTMODE"=>"auto" }
    #
    # {#Add}, {#Edit} and {#Delete} copy the requested device info
    # (via {#Select}) to {#Name} and {#Current}, {#Commit} puts it back.

    def main
      textdomain "base"

      Yast.import "Arch"
      Yast.import "Map"
      Yast.import "Mode"
      Yast.import "Netmask"
      Yast.import "FileUtils"
      Yast.import "IP"

      @Name = ""

      @Current = {}

      # Interface information:
      # Devices[string type, string id] is a map with the contents of
      # ifcfg-<i>type</i>-<i>id</i>. Separating type from id is useful because
      # the type determines the fields of the interface file.
      # Multiple addresses for an interface are nested maps
      # [type, id, "_aliases", aid]
      # @see #Read
      @Devices = {}

      # Devices information
      # @see #Read
      @OriginalDevices = {}

      # Deleted devices
      @Deleted = []

      # True if devices are already read
      @initialized = false

      # Which operation is pending?
      # global
      @operation = nil
      # FIXME: used in lan/address.ycp (#17346) -> "global"

      # Predefined network card regular expressions
      @CardRegex =
        # other: irlan|lo|plip|...
        {
          "netcard" => "ath|ci|ctc|slc|dummy|bond|eth|ficon|hsi|qeth|lcs|wlan|vlan|br|tun|tap|ib|em|p|p[0-9]+p",
          "modem"   => "ppp|modem",
          "isdn"    => "isdn|ippp",
          "dsl"     => "dsl"
        }

      # Predefined network device regular expressions
      @DeviceRegex = {
        # device types
        "netcard" => @CardRegex["netcard"],
        "modem"   => @CardRegex["modem"],
        "isdn"    => @CardRegex["isdn"],
        "dsl"     => @CardRegex["dsl"],
        # device groups
        "dialup"  => @CardRegex["modem"] + "|" + @CardRegex["dsl"] + "|" + @CardRegex["isdn"]
      }

      # Types in order from fastest to slowest.
      # @see #FastestRegexps
      @FastestTypes = { 1 => "dsl", 2 => "isdn", 3 => "modem", 4 => "netcard" }

      # -------------------- components of configuration names --------------------

      # ifcfg name = type + id + alias_id
      # If id is numeric, it is not separated from type, otherwise separated by "-"
      # Id may be empty
      # Alias_id, if nonempty, is separated by alias_separator
      @ifcfg_name_regex = "^#{DEVNAME_REGEX}#{ALIAS_SEPARATOR}?#{ALIAS_REGEX}$"

      # Translates type code exposed by kernel in sysfs onto internaly used dev types.
      @TypeBySysfs = {
        "1"     => "eth",
        "24"    => "eth",
        "32"    => "ib",
        "512"   => "ppp",
        "768"   => "ipip",
        "769"   => "ip6tnl",
        "772"   => "lo",
        "776"   => "sit",
        "778"   => "gre",
        "783"   => "irda",
        "801"   => "wlan_aux",
        "65534" => "tun"
      }

      @TypeByKeyValue = ["INTERFACETYPE"]
      @TypeByKeyExistence = [
        ["ETHERDEVICE", "vlan"],
        ["WIRELESS_MODE", "wlan"],
        ["MODEM_DEVICE", "ppp"],
        ["IPOIB_MODE", "ib"]
      ]
      @TypeByValueMatch = [
        ["BONDING_MASTER", "yes", "bond"],
        ["BRIDGE", "yes", "br"],
        ["WIRELESS", "yes", "wlan"],
        ["TUNNEL", "tap", "tap"],
        ["TUNNEL", "tun", "tun"],
        ["TUNNEL", "sit", "sit"],
        ["TUNNEL", "gre", "gre"],
        ["TUNNEL", "ipip", "ipip"],
        ["PPPMODE", "pppoe", "ppp"],
        ["PPPMODE", "pppoatm", "ppp"],
        ["PPPMODE", "capi-adsl", "ppp"],
        ["PPPMODE", "pptp", "ppp"],
        ["ENCAP", "syncppp", "isdn"],
        ["ENCAP", "rawip", "isdn"]
      ]

      @SensitiveFields = [
        "WIRELESS_WPA_PASSWORD",
        "WIRELESS_WPA_PSK",
        # the unnumbered one should be empty but just in case
        "WIRELESS_KEY",
        "WIRELESS_KEY_0",
        "WIRELESS_KEY_1",
        "WIRELESS_KEY_2",
        "WIRELESS_KEY_3"
      ]
    end

    def IsEmpty(value)
      value.nil? ? true : value.empty?
    end

    # Detects a subtype of Ethernet device type according /sys or /proc content
    #
    # @example
    #   GetEthTypeFromSysfs("eth0") -> "eth"
    #   GetEthTypeFromSysfs("bond0") -> "bon"
    #
    # @param [String] dev interface name
    # @return [String] device type
    def GetEthTypeFromSysfs(dev)
      sys_dir_path = "/sys/class/net/#{dev}"

      if FileUtils.Exists("#{sys_dir_path}/wireless") ||
          FileUtils.Exists("#{sys_dir_path}/phy80211")
        "wlan"
      elsif FileUtils.Exists("#{sys_dir_path}/bridge")
        "br"
      elsif FileUtils.Exists("#{sys_dir_path}/bonding")
        "bond"
      elsif FileUtils.Exists("#{sys_dir_path}/tun_flags")
        "tap"
      elsif FileUtils.Exists("/proc/net/vlan/#{dev}")
        "vlan"
      elsif FileUtils.Exists("/sys/devices/virtual/net/#{dev}") && dev =~ /dummy/
        "dummy"
      else
        "eth"
      end
    end

    # Detects a subtype of InfiniBand device type according /sys
    #
    # @example
    #   GetEthTypeFromSysfs("ib0") -> "ib"
    #   GetEthTypeFromSysfs("bond0") -> "bon"
    #   GetEthTypeFromSysfs("ib0.8001") -> "ibchild"
    #
    # @param [String] dev interface name
    # @return [String] device type
    def GetIbTypeFromSysfs(dev)
      sys_dir_path = "/sys/class/net/#{dev}"

      if FileUtils.Exists("#{sys_dir_path}/bonding")
        "bond"
      elsif FileUtils.Exists("#{sys_dir_path}/create_child")
        "ib"
      else
        "ibchild"
      end
    end

    # Determines device type according /sys/class/net/<dev>/type value
    #
    # Firstly, it uses /sys/class/net/<dev>/type for basic decision. Obtained values are translated to
    # device type according <kernel src>/include/uapi/linux/if_arp.h. Sometimes it uses some other checks
    # to specify a "subtype". E.g. in case of "eth" it checks for presence of "wireless" subdir to
    # determine "wlan" device.
    #
    # @param [String] dev interface name
    # @return return device type or nil if nothing known found
    def GetTypeFromSysfs(dev)
      sys_dir_path = Builtins.sformat("/sys/class/net/%1", dev)
      sys_type_path = Builtins.sformat("%1/type", sys_dir_path)

      return nil if IsEmpty(dev) || !FileUtils.Exists(sys_type_path)

      sys_type = Convert.to_string(
        SCR.Read(path(".target.string"), sys_type_path)
      )
      sys_type = if sys_type
        Builtins.regexpsub(sys_type, "(.*)\n", "\\1")
      else
        ""
      end

      sys_type = String.CutBlanks(sys_type)

      type = case sys_type
      when "1"
        GetEthTypeFromSysfs(dev)
      when "32"
        GetIbTypeFromSysfs(dev)
      else
        Ops.get(@TypeBySysfs, sys_type)
      end

      Builtins.y2debug(
        "GetTypeFromSysFs: device='%1', sysfs type='%2', type='%3'",
        dev,
        sys_type,
        type
      )

      return nil if IsEmpty(type)

      type
    end

    # Detects device type according given ifcfg configuration
    #
    # @return device type or nil if type cannot be recognized from ifcfg config
    def GetTypeFromIfcfg(ifcfg)
      ifcfg = deep_copy(ifcfg)
      type = nil

      return nil if IsEmpty(ifcfg)

      Builtins.foreach(@TypeByValueMatch) do |key_type|
        rule_key = Ops.get(key_type, 0, "")
        rule_value = Ops.get(key_type, 1, "")
        rule_type = Ops.get(key_type, 2, "")
        type = rule_type if (ifcfg[rule_key] || "").casecmp?(rule_value)
      end

      Builtins.foreach(@TypeByKeyExistence) do |key_type|
        rule_key = Ops.get(key_type, 0, "")
        rule_type = Ops.get(key_type, 1, "")
        type = rule_type if Ops.get_string(ifcfg, rule_key, "") != ""
      end

      Builtins.foreach(@TypeByKeyValue) do |rule_key|
        rule_type = Ops.get_string(ifcfg, rule_key, "")
        type = rule_type if rule_type != ""
      end

      type
    end

    # Detects device type according its name and ifcfg configuration.
    #
    # @param dev   device name
    # @param ifcfg device's ifcfg configuration
    # @return      device type
    def GetTypeFromIfcfgOrName(dev, ifcfg)
      ifcfg = deep_copy(ifcfg)
      return nil if IsEmpty(dev)

      type = GetTypeFromSysfs(dev)

      type = GetTypeFromIfcfg(ifcfg) if IsEmpty(type)

      # last instance - no record in sysfs, no configuration, device
      # name is not bounded to a type -> use fallbac "eth" as wicked already does
      type = "eth" if type.nil?

      log.debug("GetTypeFromIfcfgOrName: device='#{dev}' type='#{type}'")

      type
    end

    # Detects device type according cached data
    #
    # If cached ifcfg for given device is found it is used as parameter for
    # GetTypeFromIfcfgOrName( dev, ifcfg). Otherwise is device handled as unconfigured
    # and result is equal to GetTypeFromIfcfgOrName( dev, nil)
    #
    # @param dev   device name
    # @return      detected device type
    def GetType(dev)
      type = GetTypeFromIfcfgOrName(dev, nil)

      Builtins.foreach(@Devices) do |_dev_type, confs|
        ifcfg = Ops.get(confs, dev, {})
        type = GetTypeFromIfcfgOrName(dev, ifcfg) if !IsEmpty(ifcfg)
      end

      type
    end

    # Return device type in human readable form :-)
    # @param [String] dev device
    # @return device type
    # @example GetDeviceTypeName(eth-bus-pci-0000:01:07.0) -> "Network Card"
    # @example GetDeviceTypeName(modem0) -> "Modem"
    def GetDeviceTypeName(dev)
      # pppN must be tried before pN, modem before netcard
      if Builtins.regexpmatch(
        dev,
        Ops.add("^", Ops.get(@DeviceRegex, "modem", ""))
      )
        _("Modem")
      elsif Builtins.regexpmatch(
        dev,
        Ops.add("^", Ops.get(@DeviceRegex, "netcard", ""))
      )
        _("Network Card")
      elsif Builtins.regexpmatch(
        dev,
        Ops.add("^", Ops.get(@DeviceRegex, "isdn", ""))
      )
        _("ISDN")
      elsif Builtins.regexpmatch(
        dev,
        Ops.add("^", Ops.get(@DeviceRegex, "dsl", ""))
      )
        _("DSL")
      else
        _("Unknown")
      end
    end

    # Create a device name from its type and number
    # @param [String] typ device type
    # @param [String] num device number
    # @return device name
    # @example device_name("eth", "1") -> "eth1"
    # @example device_name("lo", "") -> "lo"
    def device_name(typ, num)
      if typ.nil? || typ == ""
        Builtins.y2error("wrong type: %1", typ)
        return nil
      end
      if num.nil?
        Builtins.y2error("wrong number: %1", num)
        return nil
      end
      return Builtins.sformat("%1%2", typ, num) if Builtins.regexpmatch(num, "^[0-9]*$")

      Builtins.sformat("%1-%2", typ, num)
    end

    # Extracts device name from alias name
    #
    # alias_name := <device_name>{ALIAS_SEPARATOR}<alias_name>
    def device_name_from_alias(alias_name)
      alias_name.sub(/#{ALIAS_SEPARATOR}.*/, "")
    end

    # Create a alias name from its type and numbers
    # @param [String] typ device type
    # @param [String] num device number
    # @param [String] anum alias number
    # @return alias name
    # @example alias_name("eth", "1", "2") -> "eth1#2"
    def alias_name(typ, num, anum)
      if typ.nil? || typ == ""
        Builtins.y2error("wrong type: %1", typ)
        return nil
      end
      if num.nil? # || num < 0
        Builtins.y2error("wrong number: %1", num)
        return nil
      end
      if anum.nil? || anum == ""
        Builtins.y2error("wrong alias number: %1", anum)
        return nil
      end
      Builtins.sformat("%1#%2", device_name(typ, num), anum)
    end

    # @deprecated Formerly hotpluggable devices required a special ifcfg name
    # @return false
    def IsHotplug(_type)
      false
    end

    # Test whether device is connected (Link:up)
    # The info is taken from sysfs
    # @param [String] dev unique device string
    # @return true if connected
    def IsConnected(dev)
      # Assume all devices are connected in testsuite mode
      return true if Mode.testsuite

      cmd = "/usr/bin/cat /sys/class/net/#{dev.shellescape}/carrier"

      ret = Convert.to_map(SCR.Execute(path(".target.bash_output"), cmd))
      Builtins.y2milestone("Sysfs returned %1", ret)

      Builtins.deletechars(Ops.get_string(ret, "stdout", ""), "\n") == "1"
    end

    # @deprecated hotpluggable devices no longer need a special type
    # Return real type of the device (incl. PCMCIA, USB, ...)
    # @param [String] type basic device type
    # @param [String] hotplug hot plug type
    # @return real type
    # @example RealType("eth", "usb") -> "eth"
    def RealType(type, _hotplug)
      if type == "" || type.nil?
        Builtins.y2error("Wrong type: %1", type)
        return "eth"
      end
      type
    end

    # ---------------------------------------------------------------------------

    # STARTMODE: onboot, on and boot are aliases for auto
    def CanonicalizeStartmode(ifcfg)
      ifcfg = deep_copy(ifcfg)
      canonicalize_startmode = {
        "on"     => "auto",
        "boot"   => "auto",
        "onboot" => "auto"
      }
      startmode = Ops.get_string(ifcfg, "STARTMODE", "")
      Ops.set(
        ifcfg,
        "STARTMODE",
        Ops.get(canonicalize_startmode, startmode, startmode)
      )
      deep_copy(ifcfg)
    end

    #
    # Canonicalize static ip configuration obtained from sysconfig. (suse#46885)
    #
    # Static ip configuration formats supported by sysconfig:
    # 1) IPADDR=10.0.0.1/8
    # 2) IPADDR=10.0.0.1 PREFIXLEN=8
    # 3) IPADDR=10.0.0.1 NETMASK=255.0.0.0
    #
    # Features:
    # - IPADDR (in form <ip>/<prefix>) overrides PREFIXLEN,
    # - NETMASK is used only if prefix length unspecified)
    # - If prefix length and NETMASK are unspecified, 32 is implied.
    #
    # Canonicalize it to:
    # - IPADDR="<ipv4>" PREFIXLEN="<prefix>" NETMASK="<netmask>") in case of IPv4 config
    # E.g. IPADDR=10.0.0.1 PREFIXLEN=8 NETMASK=255.0.0.0
    # - IPADDR="<ipv6>" PREFIXLEN="<prefix>" NETMASK="") in case of IPv6 config
    # E.g. IPADDR=2001:15c0:668e::5 PREFIXLEN=48 NETMASK=""
    #
    # @param ifcfg     a map with netconfig (ifcfg) configuration for a one device
    # @return          a map with IPADDR, NETMASK and PREFIXLEN adjusted if IPADDR is present.
    #                  Returns original ifcfg if IPADDR is not present. In case of error,
    #                  returns nil.
    #
    def CanonicalizeIP(ifcfg)
      ifcfg = deep_copy(ifcfg)
      return nil if ifcfg.nil?

      ipaddr, prefixlen = ifcfg["IPADDR"].to_s.split("/")

      return ifcfg if ipaddr.to_s == "" # DHCP or inconsistent

      prefixlen = ifcfg["PREFIXLEN"].to_s if prefixlen.to_s == ""

      prefixlen = Netmask.ToBits(ifcfg["NETMASK"].to_s).to_s if prefixlen == ""

      # Now we have ipaddr and prefixlen
      # Let's compute the rest
      netmask = IP.Check4(ipaddr) ? Netmask.FromBits(prefixlen.to_i) : ""

      ifcfg["IPADDR"] = ipaddr
      ifcfg["PREFIXLEN"] = prefixlen
      ifcfg["NETMASK"] = netmask

      ifcfg
    end

    # Filters out INTERFACETYPE option from ifcfg config when it is not needed.
    #
    # INTERFACETYPE has big impact on wicked even yast behavior. It was overused
    # by yast in the past. According wicked team it makes sense to use it only
    # in two cases 1) lo device (when it's name is changed - very strongly discouraged)
    # 2) dummy device
    #
    # This function silently modifies user's config files. However, it should make sense
    # because:
    # - INTERFACETYPE is usually not needed
    # - other functions in this module modifies the config as well (see Canonicalize* functions)
    # - using INTERFACETYPE is reported as a warning by wicked (it asks for reporting a bug)
    # - it is often ignored by wicked
    def filter_interfacetype(devmap)
      ret = deep_copy(devmap)
      ret.delete_if { |k, v| k == "INTERFACETYPE" && !["lo", "dummy"].include?(v) }
    end

    # Conceal secret information, such as WEP keys, so that the output
    # can be passed to y2log and bugzilla.
    # @param [Hash{String => Object}] ifcfg one ifcfg
    # @return ifcfg with secret fields masked out
    def ConcealSecrets1(ifcfg)
      ifcfg = deep_copy(ifcfg)
      return nil if ifcfg.nil?

      secret_fields = ifcfg.select { |k, v| @SensitiveFields.include?(k) && v != "" }
      secret_fields.map { |k, _v| ifcfg[k] = "CONCEALED" }

      ifcfg
    end

    # Conceal secret information, such as WEP keys, so that the output
    # can be passed to y2log and bugzilla. (#65741)
    # @param [Hash] devs a two-level map of ifcfgs like Devices
    # @return ifcfgs with secret fields masked out
    def ConcealSecrets(devs)
      devs = deep_copy(devs)
      return nil if devs.nil?

      out = Builtins.mapmap(
        Convert.convert(
          devs,
          from: "map",
          to:   "map <string, map <string, map <string, any>>>"
        )
      ) do |t, tdevs|
        tout = Builtins.mapmap(tdevs) do |id, ifcfg|
          { id => ConcealSecrets1(ifcfg) }
        end
        { t => tout }
      end
      deep_copy(out)
    end

    # Canonicalize IPADDR and STARTMODE of given config
    # and nested _aliases
    #
    # @param [Hash] config a map with netconfig (ifcfg) configuration
    # @return [Hash] ifcfg with canonicalized IP addresses
    def canonicalize_config(config)
      config = deep_copy(config)
      # canonicalize, #46885
      (config["_aliases"] || {}).tap do |aliases|
        aliases.each { |a, c| aliases[a] = CanonicalizeIP(c) }
      end

      config = CanonicalizeIP(config)
      CanonicalizeStartmode(config)
    end

    # Variables which could be suffixed and thus duplicated
    LOCALS = [
      "IPADDR",
      "REMOTE_IPADDR",
      "NETMASK",
      "PREFIXLEN",
      "BROADCAST",
      "SCOPE",
      "LABEL",
      "IP_OPTIONS"
    ].freeze

    # It reads, parses and transforms the given attributes
    # from the given device config path returning a hash
    # with the transformed device config
    #
    # @param [String] pth path of the device config to read from
    # @param [Hash<String, Object>] values new device map
    #
    # @return [Hash{String => Object}] parsed configuration
    def generate_config(pth, values)
      config = {}
      values.each do |val|
        item = SCR.Read(path("#{pth}.#{val}"))
        log.debug("item=#{item}")
        next if item.nil?

        # No underscore '_' -> global
        # Also temporarily standard globals
        if !val.include?("_") || LOCALS.include?(val)
          config[val] = item
          next
        end

        # Try to strip _suffix
        # @example "IP_OPTIONS_1".rpartition("_") => ["IP_OPTIONS", "_", "1"]
        v, _j, s = val.rpartition("_")
        log.info("#{val}:#{v}:#{s}")
        # Global
        if LOCALS.include?(v)
          config["_aliases"] ||= {}
          config["_aliases"][s] ||= {}
          config["_aliases"][s][v] = item
        else
          config[val] = item
        end
      end
      log.info("config=#{ConcealSecrets1(config)}")
      config = canonicalize_config(config)
      filter_interfacetype(config)
    end

    # Adapts the interface configuration used during many year for enslaved
    # interfaces (IPADDR == 0.0.0.0 and BOOTPROTO == 'static').
    #
    # Sets the BOOTPROTO as none, empties the IPADDR, and also empties the
    # NETMASK and the PREFIXLEN if exist.
    def adapt_old_config!
      @Devices.each do |devtype, devices|
        devices.each do |device, config|
          bootproto = config["BOOTPROTO"] || "static"
          next unless bootproto == "static" && config["IPADDR"] == "0.0.0.0"

          config["BOOTPROTO"] = "none"
          config["IPADDR"]    = ""
          config["NETMASK"]   = "" if config.key? "NETMASK"
          config["PREFIXLEN"] = "" if config.key? "PREFIXLEN"

          @Devices[devtype][device] = config
        end
      end

      @Devices
    end

    # The device is added to @Devices[devtype] hash using the device name as key
    # and the ifconfg hash as value
    #
    # @param [String] device name
    # @param [Hash] ifcfg a map with netconfig (ifcfg) configuration
    def add_device(device, ifcfg)
      # if possible use dev type as available in /sys otherwise use ifcfg config
      # as a fallback for device type detection
      devtype = GetTypeFromIfcfgOrName(device, ifcfg)
      @Devices[devtype] ||= {}
      @Devices[devtype][device] = ifcfg
    end

    # Read devices from files and cache it
    # @return true if sucess
    def Read
      return true if @initialized == true

      @Devices = {}

      # preparation
      devices = get_devices(ignore_confs_regex)

      # Read devices
      devices.each do |device|
        pth = ".network.value.\"#{device}\""
        log.debug("pth=#{pth}")

        values = SCR.Dir(path(pth))
        log.debug("values=#{values}")

        config = generate_config(pth, values)

        add_device(device, config)
      end
      log.debug("Devices=#{@Devices}")

      @OriginalDevices = deep_copy(@Devices)
      @initialized = true
    end

    # re-read all settings again from system
    # for creating new proposal from scratch (#170558)
    def CleanCacheRead
      @initialized = false
      Read()
    end

    # Returns a hash with configuration for particular device
    #
    # Hash map is direct maping of sysconfig file into hash.
    # Keys are sysconfig options (e.g. { 'IPADDR' => '1.1.1.1' }
    #
    # @param [String] name is device name as provided by the
    #                 system (e.g. eth0)
    # @return [Hash] device configuration or nil in case of error
    def devmap(name)
      Devices().fetch(GetType(name), {})[name]
    end

    # Returns all the devices which device name matchs given devregex
    #
    # @param [Array] devices of Devices
    # @param [String] devregex regex to filter by
    # @return [Array] of Devices that match the given regex
    def Filter(devices, devregex)
      devices = deep_copy(devices)
      return devices if devices.nil? || devregex.nil? || devregex == ""

      regex = "^(#{@DeviceRegex[devregex] || devregex})[0-9]*$"
      log.debug("regex=#{regex}")
      devices.select! { |f, _d| f =~ /#{regex}/ }
      log.debug("devices=#{devices}")
      devices
    end

    # Used in BuildSummary, BuildOverview
    def FilterDevices(devregex)
      Filter(@Devices, devregex)
    end

    # Returns all the devices that does not match the given devregex
    #
    # @param [Array] devices of Devices
    # @param [String] devregex regex to filter by
    # @return [Array] of Devices that match the given regex
    def FilterNOT(devices, devregex)
      return {} if devices.nil? || devregex.nil? || devregex == ""

      devices = deep_copy(devices)

      regex = "^(#{@DeviceRegex[devregex] || devregex})[0-9]*$"

      log.debug("regex=#{regex}")
      devices.reject! { |f, _d| f =~ /#{regex}/ }

      log.debug("devices=#{devices}")
      devices
    end

    def Write(devregex)
      log.info("Writing configuration")
      log.debug("Devices=#{@Devices}")
      log.debug("Deleted=#{@Deleted}")

      devs = Filter(@Devices, devregex)
      original_devs = Filter(@OriginalDevices, devregex)
      log.info("OriginalDevs=#{ConcealSecrets(original_devs)}")
      log.info("Devs=#{ConcealSecrets(devs)}")

      # Check for changes
      if devs == original_devs
        log.info("No changes to #{devregex} devices -> nothing to write")
        return true
      end

      # remove deleted devices
      log.info("Deleted=#{@Deleted}")
      @Deleted.each do |d|
        iface, alias_num = d.split("#")
        alias_num ? delete_alias(original_devs, iface, alias_num) : delete_device(iface)
      end

      @Deleted = []

      # write all devices
      Builtins.maplist(
        Convert.convert(
          devs,
          from: "map",
          to:   "map <string, map <string, map <string, any>>>"
        )
      ) do |typ, devsmap|
        Builtins.maplist(devsmap) do |config, devmap|
          next if devmap == Ops.get_map(original_devs, [typ, config], {})

          # write sysconfig
          p = Ops.add(Ops.add(".network.value.\"", config), "\".")
          if Ops.greater_than(
            Builtins.size(Ops.get_string(devmap, "IPADDR", "")),
            0
          ) &&
              Builtins.find(Ops.get_string(devmap, "IPADDR", ""), "/") == -1
            if Ops.greater_than(
              Builtins.size(Ops.get_string(devmap, "IPADDR", "")),
              0
            ) &&
                Ops.greater_than(
                  Builtins.size(Ops.get_string(devmap, "NETMASK", "")),
                  0
                )
              Ops.set(
                devmap,
                "IPADDR",
                Builtins.sformat(
                  "%1/%2",
                  Ops.get_string(devmap, "IPADDR", ""),
                  Netmask.ToBits(Ops.get_string(devmap, "NETMASK", ""))
                )
              )
              devmap = Builtins.remove(devmap, "NETMASK")
              # TODO : delete NETMASK from config file
            elsif Ops.greater_than(
              Builtins.size(Ops.get_string(devmap, "IPADDR", "")),
              0
            ) &&
                Ops.greater_than(
                  Builtins.size(Ops.get_string(devmap, "PREFIXLEN", "")),
                  0
                )
              Ops.set(
                devmap,
                "IPADDR",
                Builtins.sformat(
                  "%1/%2",
                  Ops.get_string(devmap, "IPADDR", ""),
                  Ops.get_string(devmap, "PREFIXLEN", "")
                )
              )
              devmap = Builtins.remove(devmap, "PREFIXLEN")
              # TODO : delete PREFIXLEN from config file
            end
          end
          # write all keys to config
          Builtins.maplist(
            Convert.convert(
              Map.Keys(devmap),
              from: "list",
              to:   "list <string>"
            )
          ) do |k|
            # Write aliases
            if k == "_aliases"
              Builtins.maplist(Ops.get_map(devmap, k, {})) do |anum, amap|
                # Normally defaulting the label would be done
                # when creating the map, not here when
                # writing, but we create it in 2 ways so it's
                # better here. Actually it does not work because
                # the edit dialog nukes LABEL :-(
                if Ops.greater_than(Builtins.size(Ops.get(amap, "IPADDR", "")), 0) &&
                    Ops.greater_than(
                      Builtins.size(Ops.get(amap, "NETMASK", "")),
                      0
                    )
                  Ops.set(
                    amap,
                    "IPADDR",
                    Builtins.sformat(
                      "%1/%2",
                      Ops.get(amap, "IPADDR", ""),
                      Netmask.ToBits(Ops.get(amap, "NETMASK", ""))
                    )
                  )
                  amap = Builtins.remove(amap, "NETMASK")
                  # TODO : delete NETMASK from config file
                elsif Ops.greater_than(
                  Builtins.size(Ops.get(amap, "IPADDR", "")),
                  0
                ) &&
                    Ops.greater_than(
                      Builtins.size(Ops.get(amap, "PREFIXLEN", "")),
                      0
                    )
                  Ops.set(
                    amap,
                    "IPADDR",
                    Builtins.sformat(
                      "%1/%2",
                      Ops.get(amap, "IPADDR", ""),
                      Ops.get(amap, "PREFIXLEN", "")
                    )
                  )
                  amap = Builtins.remove(amap, "PREFIXLEN")
                  # TODO : delete PREFIXLEN from config file
                end
                Builtins.maplist(amap) do |ak, av|
                  akk = Ops.add(Ops.add(ak, "_"), anum)
                  SCR.Write(Builtins.topath(Ops.add(p, akk)), av) #          seen_label = seen_label || ak == "LABEL";
                end
              end
            else
              # Write regular keys
              SCR.Write(
                Builtins.topath(Ops.add(p, k)),
                Ops.get_string(devmap, k, "")
              )
            end
          end

          # 0600 if contains encryption key (#24842)
          has_key = @SensitiveFields.any? { |k| devmap[k] && !devmap[k].empty? }
          if has_key
            log.debug("Permission change: #{config}")
            SCR.Write(
              Builtins.add(path(".network.section_private"), config),
              true
            )
          end
          @OriginalDevices = {} if @OriginalDevices.nil?
          Ops.set(@OriginalDevices, typ, {}) if Ops.get(@OriginalDevices, typ).nil?
          Ops.set(
            @OriginalDevices,
            [typ, config],
            Ops.get(@Devices, [typ, config], {})
          )
        end
      end

      # Finish him
      SCR.Write(path(".network"), nil)
      # Reread all settings to avoid wrong values when reopen the network
      # dialog during installation (bsc#1166778)
      CleanCacheRead()

      true
    end

    # Import data
    #
    # All devices which confirms to <devregex> are silently removed from Devices
    # and replaced by those supplied by <devices>.
    #
    # @param [String] devregex filter for devices
    # @param [Array] devices devices to replace filtered ones
    # @return true on success
    def Import(devregex, devices)
      devices = deep_copy(devices)
      devs = FilterNOT(@Devices, devregex)
      Builtins.y2debug("Devs=%1", devs)

      devices = Builtins.mapmap(devices) do |typ, devsmap|
        {
          typ => Builtins.mapmap(
            Convert.convert(
              devsmap,
              from: "map",
              to:   "map <string, map <string, any>>"
            )
          ) do |num, config|
            config = CanonicalizeIP(config)
            config = CanonicalizeStartmode(config)
            { num => config }
          end
        }
      end

      @Devices = Convert.convert(
        Builtins.union(devs, devices),
        from: "map",
        to:   "map <string, map <string, map <string, any>>>"
      )

      @initialized = !(devices.nil? || devices == {})

      Builtins.y2milestone(
        "NetworkInterfaces::Import - done, cache content: %1",
        @Devices
      )

      true
    end

    # @deprecated Not longer needed
    # Return supported network device types (for type netcard)
    # for this hardware
    def GetDeviceTypes
      # common linux device types available on all architectures
      common_dev_types = ["eth", "vlan", "br", "tun", "tap", "bond"]

      # s390 specific device types
      s390_dev_types = ["hsi", "ctc", "ficon", "qeth", "lcs"]

      # device types which cannot be present on s390 arch
      s390_unknown_dev_types = [
        "dummy",
        "wlan",
        "ib"
      ]

      dev_types = common_dev_types + (Arch.s390 ? s390_dev_types : s390_unknown_dev_types)

      Builtins.foreach(dev_types) do |device|
        if !Builtins.contains(
          Builtins.splitstring(Ops.get(@DeviceRegex, "netcard", ""), "|"),
          device
        )
          Builtins.y2error(
            "%1 is not contained in DeviceRegex[\"netcard\"]",
            device
          )
        end
      end

      deep_copy(dev_types)
    end

    # @deprecated Not longer needed
    # Return textual device type
    # @param [String] type device type
    # @param [String] longdescr description type
    # @return textual form of device type
    # @example
    #   GetDevTypeDescription("eth", false) -> "Ethernet"
    # @example
    #   GetDevTypeDescription("eth", true)  -> "Ethernet Network Card"
    def GetDevTypeDescription(type, longdescr)
      if Builtins.issubstring(type, "#")
        # Device type label
        # This is what used to be Virtual Interface (eth0:1).
        # In our data model, additional addresses for an interface
        # are represented as its sub-interfaces.
        # And also we frequently confuse "device" and "interface"
        # :-(
        return _("Additional Address")
      end

      device_types = {
        # Device type label
        "atm"   => [
          _("ATM"),
          _("Asynchronous Transfer Mode (ATM)")
        ],
        # Device type label
        "bond"  => [_("Bond"), _("Bond Network")],
        # Device type label
        "ci"    => [
          _("CLAW"),
          _("Common Link Access for Workstation (CLAW)")
        ],
        # Device type label
        "ctc"   => [
          _("CTC"),
          _("Channel to Channel Interface (CTC)")
        ],
        # Device type label
        "dsl"   => [_("DSL"), _("DSL Connection")],
        # Device type label
        "dummy" => [_("Dummy"), _("Dummy Network Device")],
        # Device type label
        "eth"   => [
          _("Ethernet"),
          _("Ethernet Network Card")
        ],
        # Device type label
        "ficon" => [
          _("FICON"),
          _("Fiberchannel System Connector (FICON)")
        ],
        # Device type label
        "hippi" => [
          _("HIPPI"),
          _("HIgh Performance Parallel Interface (HIPPI)")
        ],
        # Device type label
        "hsi"   => [
          _("Hipersockets"),
          _("Hipersockets Interface (HSI)")
        ],
        # Device type label
        "ippp"  => [_("ISDN"), _("ISDN Connection")],
        # Device type label
        "irlan" => [_("IrDA"), _("Infrared Network Device")],
        # Device type label
        "irda"  => [_("IrDA"), _("Infrared Device")],
        # Device type label
        "isdn"  => [_("ISDN"), _("ISDN Connection")],
        # Device type label
        "lcs"   => [_("OSA LCS"), _("OSA LCS Network Card")],
        # Device type label
        "lo"    => [_("Loopback"), _("Loopback Device")],
        # Device type label
        "modem" => [_("Modem"), _("Modem")],
        # Device type label
        "net"   => [_("ISDN"), _("ISDN Connection")],
        # Device type label
        "plip"  => [
          _("Parallel Line"),
          _("Parallel Line Connection")
        ],
        # Device type label
        "ppp"   => [_("Modem"), _("Modem")],
        # Device type label
        "qeth"  => [
          _("QETH"),
          _("OSA-Express or QDIO Device (QETH)")
        ],
        # Device type label
        "sit"   => [
          _("IPv6-in-IPv4"),
          _("IPv6-in-IPv4 Encapsulation Device")
        ],
        # Device type label
        "slip"  => [
          _("Serial Line"),
          _("Serial Line Connection")
        ],
        # Device type label
        "vmnet" => [_("VMWare"), _("VMWare Network Device")],
        # Device type label
        "wlan"  => [
          _("Wireless"),
          _("Wireless Network Card")
        ],
        # Device type label
        "vlan"  => [_("VLAN"), _("Virtual LAN")],
        # Device type label
        "br"    => [_("Bridge"), _("Network Bridge")],
        # Device type label
        "tun"   => [_("TUN"), _("Network TUNnel")],
        # Device type label
        "tap"   => [_("TAP"), _("Network TAP")],
        # Device type label
        "ib"    => [_("InfiniBand"), _("InfiniBand Device")]
      }

      if Builtins.haskey(device_types, type)
        return Ops.get_string(
          device_types,
          [type, (longdescr == true) ? 1 : 0],
          ""
        )
      end

      type1 = String.FirstChunk(type, "-")
      if Builtins.haskey(device_types, type1)
        return Ops.get_string(
          device_types,
          [type1, (longdescr == true) ? 1 : 0],
          ""
        )
      end

      Builtins.y2error("Unknown type: %1", type)
      type
    end

    # Export data
    # @return dumped settings (later acceptable by Import())
    def Export(devregex)
      devs = Filter(@Devices, devregex)
      log.debug("Devs=#{devs}")
      devs
    end

    # Were the devices changed?
    # @return true if modified
    def Modified(devregex)
      devs = Filter(@Devices, devregex)
      original_devs = Filter(@OriginalDevices, devregex)
      log.debug("OriginalDevs=#{original_devs}")
      log.debug("Devs=#{devs}")
      devs == original_devs
    end

    # Check presence of the device (alias)
    # @param [String] dev device identifier
    # @return true if device is present
    def Check(dev)
      Builtins.y2debug("Check(%1)", dev)
      typ = GetType(dev)
      return false if !Builtins.haskey(@Devices, typ)

      devsmap = Ops.get(@Devices, typ, {})
      return false if !Builtins.haskey(devsmap, dev)

      Builtins.y2debug("Check passed")
      true
    end

    # Select the given device
    # @param [String] name device to select ("" for new device, default values)
    # @return true if success
    def Select(name)
      @Name = ""
      @Current = {}

      Builtins.y2debug("name=%1", name)
      if name != "" && !Check(name)
        Builtins.y2error("No such device: %1", name)
        return false
      end

      @Name = name
      t = GetType(@Name)
      @Current = Ops.get(@Devices, [t, @Name], {})

      Builtins.y2debug("Name=%1", @Name)
      Builtins.y2debug("Current=%1", @Current)

      true
    end

    # Add a new device
    # @return true if success
    def Add
      @operation = nil
      return false if Select("") != true

      @operation = :add
      true
    end

    # Edit the given device
    # @param [String] name device to edit
    # @return true if success
    def Edit(name)
      @operation = nil
      return false if Select(name) != true

      @operation = :edit
      true
    end

    # Delete the given device
    # @param [String] name device to delete
    # @return true if success
    def Delete(name)
      @operation = nil
      return false if Select(name) != true

      @operation = :delete
      true
    end

    # Update Devices map
    # @param [String] name device identifier
    # @param [Hash<String, Object>] newdev new device map
    # @param [true, false] check if check if device already exists
    # @return true if success
    def Change2(name, newdev, check)
      newdev = deep_copy(newdev)
      Builtins.y2debug("Change(%1,%2,%3)", name, newdev, check)
      Builtins.y2debug("Devices=%1", @Devices)
      if Check(name) && check
        Builtins.y2error("Device already present: %1", name)
        return false
      end

      t = if IsEmpty(newdev)
        GetType(name)
      else
        GetTypeFromIfcfgOrName(name, newdev)
      end

      if name == @Name
        int_type = Ops.get_string(@Current, "INTERFACETYPE", "")

        t = int_type if Ops.greater_than(Builtins.size(int_type), 0)
      end
      Builtins.y2debug("ChangeDevice(%1)", name)

      # newdev is already a deep_copy created above
      @Devices[t] = @Devices.fetch(t, {}).merge(name => newdev)

      Builtins.y2debug("Devices=%1", @Devices)
      true
    end

    def Delete2(name)
      if !Check(name)
        Builtins.y2error("Device not found: %1", name)
        return false
      end

      t = GetType(name)
      devsmap = Ops.get(@Devices, t, {})

      devsmap = Builtins.remove(devsmap, name)

      Ops.set(@Devices, t, devsmap)

      # Originally this avoided errors in the log when deleting an
      # interface that was not present at Read (had no ifcfg file).
      # #115448: OriginalDevices is not updated after Write so
      # returning to the network proposal and deleting a card would not work.
      Builtins.y2milestone("Deleting file: #{name}")
      @Deleted << name
      true
    end

    # Add the alias to the list of deleted items.
    # Called when exiting from the aliases-of-device dialog.
    # #48191
    def DeleteAlias(device, aid)
      alias_ = Builtins.sformat("%1#%2", device, aid)
      Builtins.y2milestone("Deleting alias: %1", alias_)
      @Deleted << alias_
      true
    end

    def Commit
      Builtins.y2debug("Name=%1", @Name)
      Builtins.y2debug("Current=%1", @Current)
      Builtins.y2debug("Devices=%1", @Devices)
      Builtins.y2debug("Deleted=%1", @Deleted)
      Builtins.y2debug("operation=%1", @operation)

      case @operation
      when :add, :edit
        Change2(@Name, @Current, @operation == :add)
      when :delete
        Delete2(@Name)
      else
        Builtins.y2error("Unknown operation: %1 (%2)", @operation, @Name)
        return false
      end

      Builtins.y2debug("Devices=%1", @Devices)
      Builtins.y2debug("Deleted=%1", @Deleted)

      @Name = ""
      @Current = {}
      @operation = nil

      true
    end

    def GetValue(name, key)
      return nil if !Select(name)

      Ops.get_string(@Current, key, "")
    end

    def SetValue(name, key, value)
      return nil unless Edit(name)
      return false if key.nil? || key == "" || value.nil?

      @Current[key] = value

      Commit()
    end

    # get IP addres + additional IP addresses
    # @param [String] device identifier for network interface
    # @return [Array] of IP addresses of selected interface
    def GetIP(device)
      Select(device)
      ips = [GetValue(device, "IPADDR")]
      Builtins.foreach(Ops.get_map(@Current, "_aliases", {})) do |_key, value|
        ips = Builtins.add(ips, Ops.get_string(value, "IPADDR", ""))
      end
      deep_copy(ips)
    end

    # Locate devices which attributes match given key and value
    #
    # @param [String] key device key
    # @param [String] val device value
    # @return [Array] of devices with key=val
    def Locate(key, val)
      ret = []

      @Devices.each_value do |devsmap|
        devsmap.each do |device, conf|
          ret << device if conf[key] == val
        end
      end

      ret
    end

    # @deprecated No longer needed
    # @return true
    def CleanHotplugSymlink
      true
    end

    # Get devices of the given type
    # @param [String] devregex devices type ("" for all)
    # @return [Array] of found devices
    def List(devregex)
      ret = []
      if devregex == "" || devregex.nil?
        Builtins.maplist(@Devices) do |_t, d|
          Builtins.maplist(
            Convert.convert(
              Map.Keys(d),
              from: "list",
              to:   "list <string>"
            )
          ) { |device| Ops.set(ret, Builtins.size(ret), device) }
        end
      else
        # it's a regex for type, not the whole name
        regex = Ops.add(
          Ops.add("^(", Ops.get(@DeviceRegex, devregex, devregex)),
          ")$"
        )
        Builtins.maplist(@Devices) do |t, d|
          if Builtins.regexpmatch(t, regex)
            Builtins.maplist(
              Convert.convert(
                Map.Keys(d),
                from: "list",
                to:   "list <string>"
              )
            ) { |device| Ops.set(ret, Builtins.size(ret), device) }
          end
        end
      end
      ret = Builtins.filter(ret) do |row|
        next true if !row.nil?

        Builtins.y2error("Filtering out : %1", row)
        false
      end
      Builtins.y2debug("List(%1) = %2", devregex, ret)
      deep_copy(ret)
    end

    # Find the fastest available device
    def Fastest
      ret = ""
      devices = List("")

      # Find the fastest device
      Builtins.foreach(@FastestTypes) do |_num, type|
        Builtins.foreach(devices) do |dev|
          if ret == "" &&
              Builtins.regexpmatch(
                dev,
                Ops.add(Ops.add("^", Ops.get(@DeviceRegex, type, "")), "[0-9]*$")
              ) &&
              IsConnected(dev)
            ret = dev
          end
        end
      end

      Builtins.y2milestone("ret=%1", ret)
      ret
    end

    def FastestType(name)
      ret = ""
      Builtins.maplist(@FastestTypes) do |_num, type|
        regex = Ops.get(@DeviceRegex, type, "")
        if ret == "" &&
            Builtins.regexpmatch(name, Ops.add(Ops.add("^", regex), "[0-9]*$"))
          ret = type
        end
      end

      ret
    end

    # #46803: forbid "/" (filename), maybe also "-" (separator) "_" (escape)
    def ValidCharsIfcfg
      String.ValidCharsFilename
    end

  private

    # Device configuration files are matched against this regexp
    #
    # The regexp defines files which should not be parsed (e.g. ifcfg-eth0.bak)
    #
    # It is usually usefull to ignore files which various editors
    # or other tools or daemons (e.g. firewalld)
    # create as a backup (e.g. ifcfg-eth0~), also users sometime
    # creates a backup when editing files from commandline
    # (typically use extension .bak or .old)
    #
    # Moreover configuration filenames that contain the following
    # blacklisted extensions, will be ignored by wicked:
    # ~ .old .bak .orig .scpmbackup .rpmnew .rpmsave .rpmorig
    #
    # For details see bnc#1073727
    #
    # @return [Regexp] regexp describing ignored configurations
    def ignore_confs_regex
      /(\.bak|\.orig|\.rpmnew|\.rpmorig|\.rpmsave|-range|~|\.old|\.scpmbackup)$/
    end

    # Get current sysconfig configured interfaces
    #
    # @param devregex [Regexp] regexp to filter by
    # @return [Array<String>] of ifcfg names
    def get_devices(devregex)
      devices = SCR.Dir(path(".network.section")) || []

      devices.reject! { |file| file =~ devregex } unless devregex.nil?
      devices.delete_if(&:empty?)

      log.debug "devices=#{devices}"
      devices
    end

    # Convenience method to delete an interface config file from the system
    #
    # @param iface [String] interface name of the config file to be deleted
    def delete_device(iface)
      p = path(".network.section") + iface
      log.debug("deleting: #{p}")
      SCR.Write(p, nil)
    end

    # Convenience method to delete an specific ip alias from an interface
    # config file
    #
    # @param devices [Hash<String, Hash<String, Object>>] hash with the devices
    #   to remove the aliases from
    # @param iface [String] interface name of the alias which alias need to be
    #   removed
    # @param alias_num [String] index num of the alias that needs to be removed
    def delete_alias(devices, iface, alias_num)
      dev_map = devices.values.find { |d| d.keys.include?(iface) } || {}
      dev_aliases = dev_map.fetch(iface, {}).fetch("_aliases", {})

      base = path(".network.value") + iface
      # look in OriginalDevs because we need to catch all variables
      # of the alias

      dev_aliases.fetch(alias_num, {}).each_key do |key|
        p = base + "#{key}_#{alias_num}"
        log.debug("deleting: #{p}")
        SCR.Write(p, nil)
      end
    end

    publish variable: :Name, type: "string"
    publish variable: :Current, type: "map <string, any>"
    publish variable: :CardRegex, type: "map <string, string>"
    publish function: :GetTypeFromIfcfg, type: "string (map <string, any>)"
    publish function: :GetType, type: "string (string)"
    publish function: :GetDeviceTypeName, type: "string (string)"
    publish function: :IsHotplug, type: "boolean (string)"
    publish function: :IsConnected, type: "boolean (string)"
    publish function: :RealType, type: "string (string, string)"
    publish function: :CanonicalizeIP, type: "map <string, any> (map <string, any>)"
    publish function: :ConcealSecrets1, type: "map (map <string, any>)"
    publish function: :ConcealSecrets, type: "map (map)"
    publish function: :Read, type: "boolean ()"
    publish function: :CleanCacheRead, type: "boolean ()"
    publish function: :FilterDevices, type: "map <string, map> (string)"
    publish function: :Write, type: "boolean (string)"
    publish function: :Import, type: "boolean (string, map <string, map>)"
    publish function: :GetDeviceTypes, type: "list <string> ()"
    publish function: :GetDevTypeDescription, type: "string (string, boolean)"
    publish function: :Export, type: "map <string, map> (string)"
    publish function: :Check, type: "boolean (string)"
    publish function: :Select, type: "boolean (string)"
    publish function: :Add, type: "boolean ()"
    publish function: :Edit, type: "boolean (string)"
    publish function: :Delete, type: "boolean (string)"
    publish function: :Delete2, type: "boolean (string)"
    publish function: :DeleteAlias, type: "boolean (string, string)"
    publish function: :Commit, type: "boolean ()"
    publish function: :GetValue, type: "string (string, string)"
    publish function: :SetValue, type: "boolean (string, string, string)"
    publish function: :GetIP, type: "list <string> (string)"
    publish function: :Locate, type: "list <string> (string, string)"
    publish function: :CleanHotplugSymlink, type: "boolean ()"
    publish function: :List, type: "list <string> (string)"
    publish function: :Fastest, type: "string ()"
    publish function: :FastestType, type: "string (string)"
    publish function: :ValidCharsIfcfg, type: "string ()"
  end

  NetworkInterfaces = NetworkInterfacesClass.new
  NetworkInterfaces.main
end