yast/yast-network

View on GitHub
src/lib/y2network/udev_rule.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# Copyright (c) [2019] SUSE LLC
#
# 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 SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.

require "y2network/udev_rule_part"

module Y2Network
  # Simple udev rule class
  #
  # This class represents a network udev rule. The current implementation is quite simplistic,
  # featuring a an API which is tailored to our needs.
  #
  # Basically, udev rules are kept in two different files under `/etc/udev/rules.d`:
  #
  # * 70-persistent-net.rules ('net' group): rules to assign names to interfaces.
  # * 79-yast2-drivers.rules ('drivers' group): rules to assign drivers to interfaces.
  #
  # This class offers a set of constructors to build different kinds of rules.
  # See {.new_mac_based_rename}, {.new_bus_id_based_rename} and {.new_driver_assignment}.
  #
  # When it comes to write rules to the filesystem, we decided to offer different methods to
  # write to each file. See {.write_net_rules} and {.write_drivers_rules}.
  #
  # @example Create a rule containing some key/value pairs (rule part)
  #   rule = Y2Network::UdevRule.new(
  #     Y2Network::UdevRulePart.new("ATTR{address}", "==", "?*31:78:f2"),
  #     Y2Network::UdevRulePart.new("NAME", "=", "mlx4_ib3")
  #   )
  #   rule.to_s #=> "ACTION==\"add\", SUBSYSTEM==\"net\", ATTR{address}==\"?*31:78:f2\",
  #                  NAME=\"eth0\""
  #
  # @example Create a rule from a string
  #   rule = UdevRule.find_for("eth0")
  #   rule.to_s #=> "ACTION==\"add\", SUBSYSTEM==\"net\", ATTR{address}==\"?*31:78:f2\",
  #                  NAME=\"eth0\""
  #
  # @example Writing renaming rules
  #   rule = UdevRule.new_mac_based_rename("00:12:34:56:78:ab", "eth0")
  #   UdevRule.write_net_rules([rule])
  #
  # @example Writing driver assignment rules
  #   rule = UdevRule.new_driver_assignment("virtio:d00000001v00001AF4", "virtio_net")
  #   UdevRule.write_drivers_rules([rule])
  #
  class UdevRule
    class << self
      # Returns all persistent network rules
      #
      # @return [Array<UdevRule>] Persistent network rules
      def all
        naming_rules + drivers_rules
      end

      # Returns naming rules
      #
      # @return [Array<UdevRule>] Naming network rules
      def naming_rules
        find_rules(:net)
      end

      # Returns driver rules
      #
      # @return [Array<UdevRule>] Drivers rules
      def drivers_rules
        find_rules(:drivers)
      end

      # Returns the udev rule for a given device
      #
      # Only the naming rules are considered.
      #
      # @param device [String] Network device name
      # @return [UdevRule] udev rule
      def find_for(device)
        naming_rules.find { |r| r.device == device }
      end

      # Helper method to create a rename rule based on a MAC address
      #
      # @param name [String] Interface's name
      # @param mac  [String] MAC address
      def new_mac_based_rename(name, mac)
        new_network_rule(
          [
            # Guard to not try to rename everything with the same MAC address (e.g. vlan devices
            # inherit the MAC address from the underlying device).
            # FIXME: it won't work when using predictable network names (openSUSE)
            # UdevRulePart.new("KERNEL", "==", "eth*"),
            # The port number of a NIC where the ports share the same hardware device.
            UdevRulePart.new("ATTR{dev_id}", "==", "0x0"),
            UdevRulePart.new("ATTR{address}", "==", mac),
            UdevRulePart.new("NAME", "=", name)
          ]
        )
      end

      # Helper method to create a rename rule based on the BUS ID
      #
      # @param name     [String] Interface's name
      # @param bus_id   [String] BUS ID (e.g., "0000:08:00.0")
      # @param dev_port [String] Device port
      def new_bus_id_based_rename(name, bus_id, dev_port = nil)
        parts = [UdevRulePart.new("KERNELS", "==", bus_id)]
        parts << UdevRulePart.new("ATTR{dev_port}", "==", dev_port) if dev_port
        parts << UdevRulePart.new("NAME", "=", name)
        new_network_rule(parts)
      end

      # Returns a network rule
      #
      # The network rule includes some parts by default.
      #
      # @param parts [Array<UdevRulePart] Additional rule parts
      # @return [UdevRule] udev rule
      def new_network_rule(parts = [])
        base_parts = [
          UdevRulePart.new("SUBSYSTEM", "==", "net"),
          UdevRulePart.new("ACTION", "==", "add"),
          UdevRulePart.new("DRIVERS", "==", "?*"),
          # Ethernet devices
          # https://github.com/torvalds/linux/blob/bb7ba8069de933d69cb45dd0a5806b61033796a3/include/uapi/linux/if_arp.h#L31
          # TODO: what about InfiniBand (it is type 32)?
          UdevRulePart.new("ATTR{type}", "==", "1")
        ]
        new(base_parts.concat(parts))
      end

      # Returns a module assignment rule
      #
      # @param modalias    [String] Interface's modalias
      # @param driver_name [String] Module name
      # @return [UdevRule] udev rule
      def new_driver_assignment(modalias, driver_name)
        parts = [
          UdevRulePart.new("ENV{MODALIAS}", "==", modalias),
          UdevRulePart.new("ENV{MODALIAS}", "=", driver_name)
        ]
        new(parts)
      end

      # Writes udev rules to the filesystem
      #
      # @param udev_rules [Array<UdevRule>] List of udev rules
      def write_net_rules(udev_rules)
        Yast::SCR.Write(Yast::Path.new(".udev_persistent.rules"), udev_rules.map(&:to_s))
        # Writes changes to the rules file
        Yast::SCR.Write(Yast::Path.new(".udev_persistent.nil"), [])
        Yast::SCR.UnmountAgent(Yast::Path.new(".udev_persistent"))
      end

      # Writes drivers specific udev rules to the filesystem
      #
      # Those rules that does not have an MODALIAS part will be ignored.
      #
      # @param udev_rules [Array<UdevRule>] List of udev rules
      def write_drivers_rules(udev_rules)
        rules_hash = udev_rules.each_with_object({}) do |rule, hash|
          driver = rule.driver
          next unless driver

          hash[driver] = rule.parts.map(&:to_s)
        end
        Yast::SCR.Write(Yast::Path.new(".udev_persistent.drivers"), rules_hash)
        # Writes changes to the rules file
        Yast::SCR.Write(Yast::Path.new(".udev_persistent.nil"), [])
        Yast::SCR.UnmountAgent(Yast::Path.new(".udev_persistent"))
      end

      # Clears rules cache map
      def reset_cache
        @all = nil
      end

    private

      def find_rules(group)
        @all ||= {}
        return @all[group] if @all[group]

        rules_map = Yast::SCR.Read(Yast::Path.new(".udev_persistent.#{group}")) || {}
        Yast::SCR.UnmountAgent(Yast::Path.new(".udev_persistent"))
        @all[group] = rules_map.values.map do |parts|
          udev_parts = parts.map { |p| UdevRulePart.from_string(p) }.compact
          new(udev_parts)
        end
      end
    end

    # @return [Array<UdevRulePart>] Parts of the udev rule
    attr_reader :parts

    # Constructor
    #
    # @param parts [Array<UdevRulePart>] udev rule parts
    def initialize(parts = [])
      @parts = parts
    end

    # Adds a part to the rule
    #
    # @param key      [String] Key name
    # @param operator [String] Operator
    # @param value    [String] Value to match or assign
    def add_part(key, operator, value)
      @parts << UdevRulePart.new(key, operator, value)
    end

    # Returns an string representation that can be used in a rules file
    #
    # @return [String]
    def to_s
      parts.map(&:to_s).join(", ")
    end

    # Returns the part with the given key
    #
    # @param key [String] Key name to match
    # @param operator [String,nil] Operator to match; nil omits matching the operator
    def part_by_key(key, operator = nil)
      parts.find { |p| p.key == key && (operator.nil? || p.operator == operator) }
    end

    # Returns the value for a given part
    #
    # @param key [String] Key name
    # @param operator [String,nil] Operator to match; nil omits matching the operator
    # @return [String,nil] Value or nil if not found a part which such a key
    def part_value_for(key, operator = nil)
      part = part_by_key(key, operator)
      return nil unless part

      part.value
    end

    # Returns the MAC in the udev rule
    #
    # @return [String,nil] MAC address or nil if not found
    # @see #part_value_for
    def mac
      part_value_for("ATTR{address}")
    end

    # Convenience method to replace a specific part by another one. In case
    # that there is no part to be replaced then a new part is added.
    #
    # @param key      [String] Key name
    # @param operator [String] Operator
    # @param value    [String] Value to match or assign
    # @see #add_part
    def replace_part(key, operator, value)
      part = part_by_key(key, operator)
      if part
        part.value = value
      else
        add_part(key, operator, value)
      end
    end

    # Convenience method which takes care of modifing the udev rule using the
    # MAC address as the naming mechanism
    def rename_by_mac(name, address)
      parts.delete_if(&:dev_port?)
      part = part_by_key("KERNELS")
      part.key = "ATTR{address}" if part

      replace_part("ATTR{address}", "==", address) if mac != address
      ## Ensure the name is always at the end of the rule
      parts.delete_if { |p| p.dev_port? || p.name? }
      add_part("NAME", "=", name)
    end

    # Convenience method which takes care of modifing the udev rule using the
    # bus_id and the dev_port when needed as the naming mechanism
    def rename_by_bus_id(name, bus_id_value, dev_port_value = nil)
      parts.delete_if { |p| (p.dev_port? && dev_port_value.nil?) }
      part = part_by_key("ATTR{address}")
      part.key = "KERNELS" if part

      replace_part("KERNELS", "==", bus_id_value) if bus_id != bus_id_value
      replace_part("ATTR{dev_port}", "==", dev_port_value) if dev_port != dev_port_value
      ## Ensure the name is always at the end of the rule
      parts.delete_if(&:name?)
      add_part("NAME", "=", name)
    end

    # Returns the BUS ID in the udev rule
    #
    # @return [String,nil] BUS ID or nil if not found
    # @see #part_value_for
    def bus_id
      part_value_for("KERNELS")
    end

    # Returns the device port in the udev rule
    #
    # @return [String,nil] Device port or nil if not found
    # @see #part_value_for
    def dev_port
      part_value_for("ATTR{dev_port}")
    end

    # Returns the device mentioned in the rule (if any)
    #
    # @return [String,nil] Device name or nil if not found
    def device
      part_value_for("NAME", "=")
    end

    # Returns the original modalias
    #
    # @return [String,nil] Original modalias or nil if not found
    def original_modalias
      part_value_for("ENV{MODALIAS}", "==")
    end

    # Returns the modalias
    #
    # @return [String,nil] Original modalias or nil if not found
    def driver
      part_value_for("ENV{MODALIAS}", "=")
    end

    # Returns the drivers mentioned in the rule (if any)
    #
    # @return [String,nil] drivers or nil if not found
    def drivers
      part_value_for("DRIVERS", "==")
    end
  end
end