yast/yast-network

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

Summary

Maintainability
A
35 mins
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/config_writer"
require "y2network/config_reader"
require "y2network/routing"
require "y2network/dns"
require "y2network/hostname"
require "y2network/interfaces_collection"
require "y2network/s390_group_devices_collection"
require "y2network/connection_configs_collection"
require "y2network/physical_interface"
require "y2network/can_be_copied"
require "y2network/backend"
require "yast2/equatable"

module Y2Network
  # This class represents the current network configuration including interfaces,
  # routes, etc.
  #
  # @example Reading from wicked
  #   config = Y2Network::Config.from(:wicked).config
  #   config.interfaces.map(&:name) #=> ["lo", eth0", "wlan0"]
  #
  # @example Adding a default route to the first routing table
  #   config = Y2Network::Config.from(:wicked).config
  #   route = Y2Network::Route.new(to: :default)
  #   config.routing.tables.first << route
  #   config.write
  class Config
    include Yast2::Equatable
    include CanBeCopied
    include Yast::Logger

    # @return [Backend, Symbol, nil]
    attr_reader :backend
    # @return [InterfacesCollection]
    attr_accessor :interfaces
    # @return [ConnectionConfigsCollection]
    attr_accessor :connections
    # @return [S390GroupDevicesCollection]
    attr_accessor :s390_devices
    # @return [Routing] Routing configuration
    attr_accessor :routing
    # @return [DNS] DNS configuration
    attr_accessor :dns
    # @return [Hostname] Hostname configuration
    attr_accessor :hostname
    # @return [Array<Driver>] Available drivers
    attr_accessor :drivers
    # @return [Symbol] Information source (see {Y2Network::Reader} and {Y2Network::Writer})
    attr_accessor :source

    class << self
      # @param source [Symbol] Source to read the configuration from
      # @param opts   [Array<Object>] Reader options. Check readers documentation to find out
      #   supported options.
      # @return [IssuesResult] Result of reading the network configuration
      def from(source, *opts)
        reader = ConfigReader.for(source, *opts)
        reader.read
      end

      # Adds the configuration to the register
      #
      # @param id     [Symbol] Configuration ID
      # @param config [Y2Network::Config] Network configuration
      def add(id, config)
        configs[id] = config
      end

      # Finds the configuration in the register
      #
      # @param id [Symbol] Configuration ID
      # @return [Config,nil] Configuration with the given ID or nil if not found
      def find(id)
        configs[id]
      end

      # Resets the configuration register
      def reset
        configs.clear
      end

    private

      # Configuration register
      def configs
        @configs ||= {}
      end
    end

    # Constructor
    #
    # @param source [Symbol] Configuration source
    # @param opts   [Hash] configuration options
    # @option opts  [InterfacesCollection] :interfaces List of interfaces
    # @option opts  [ConnectionConfigsCollection] :connections List of connection configurations
    # @option opts  [S390GroupDevicesCollection] :s390_devices
    # @option opts  [Routing] :routing Object with routing configuration
    # @option opts  [DNS] :dns Object with DNS configuration
    # @option opts  [Hostname] :hostname Object with Hostname configuration
    # @option opts  [Array<Driver>] :drivers List of available drivers
    def initialize(source:, **opts)
      @backend = opts.fetch(:backend, nil)
      @interfaces = opts.fetch(:interfaces, InterfacesCollection.new)
      @connections = opts.fetch(:connections, ConnectionConfigsCollection.new)
      @s390_devices = opts.fetch(:s390_devices, S390GroupDevicesCollection.new)
      @drivers = opts.fetch(:drivers, [])
      @routing = opts.fetch(:routing, Routing.new)
      @dns = opts.fetch(:dns, DNS.new)
      @hostname = opts.fetch(:hostname, Hostname.new)
      @source = source
    end

    # Writes the configuration into the YaST modules
    #
    # Writes only changes against original configuration if the original configuration
    # is provided
    #
    # @param original [Y2Network::Config] configuration used for detecting changes
    # @param target   [Symbol] Target to write the configuration to (:wicked)
    # @param only [Array<symbol>, nil] explicit sections to be written, by default if no
    #   parameter is given then all changes will be written.
    #
    # @return [IssuesResult] Result of writing the network configuration
    #
    # @see Y2Network::ConfigWriter
    def write(original: nil, target: nil, only: nil)
      target = target || backend&.id || source
      Y2Network::ConfigWriter.for(target).write(self, original, only: only)
    end

    eql_attr :source, :backend, :interfaces, :routing, :dns, :hostname, :connections, :s390_devices

    # Renames a given interface and the associated connections
    #
    # @param old_name  [String] Old interface's name
    # @param new_name  [String] New interface's name
    # @param mechanism [Symbol] Property to base the rename on (:mac or :bus_id)
    def rename_interface(old_name, new_name, mechanism)
      log.info "Renaming #{old_name.inspect} to #{new_name.inspect} using #{mechanism.inspect}"
      interface = interfaces.by_name(old_name || new_name)
      interface.rename(new_name, mechanism)
      return unless old_name # do not modify configurations if it is just renaming mechanism

      connections.by_interface(old_name).each do |connection|
        connection.interface = new_name
        rename_dependencies(old_name, new_name, connection)
      end
      hostname.dhcp_hostname = new_name if hostname.dhcp_hostname == old_name
    end

    # deletes interface and all its config. If interface is physical,
    # it is not removed as we cannot remove physical interface.
    #
    # @param name [String] Interface's name
    def delete_interface(name)
      delete_dependents(name)

      connections.reject! { |c| c.interface == name }
      # do not use no longer existing device name
      hostname.dhcp_hostname = :none if hostname.dhcp_hostname == name
      interface = interfaces.by_name(name)
      return if interface.is_a?(PhysicalInterface) && interface.present?

      interfaces.reject! { |i| i.name == name }
    end

    # Adds or update a connection config
    #
    # If the interface which is associated to does not exist (because it is a virtual one or it is
    # not present), it gets added.
    def add_or_update_connection_config(connection_config)
      log.info "add_update connection config #{connection_config.inspect}"
      connections.add_or_update(connection_config)
      interface = interfaces.by_name(connection_config.interface)
      return if interface

      log.info "Creating new interface"
      interfaces << Interface.from_connection(connection_config)
    end

    # Returns the candidate drivers for a given interface
    #
    # @return [Array<Driver>]
    def drivers_for_interface(name)
      interface = interfaces.by_name(name)
      names = interface.drivers.map(&:name)
      if interface.custom_driver && !names.include?(interface.custom_driver)
        names << interface.custom_driver
      end
      drivers.select { |d| names.include?(d.name) }
    end

    # Adds or update a driver
    #
    # @param new_driver [Driver] Driver to add or update
    def add_or_update_driver(new_driver)
      idx = drivers.find_index { |d| d.name == new_driver.name }
      if idx
        drivers[idx] = new_driver
      else
        drivers << new_driver
      end
    end

    # Determines whether a given interface is configured or not
    #
    # An interface is considered as configured when it has an associated collection.
    #
    # @param iface_name [String] Interface's name
    # @return [Boolean]
    def configured_interface?(iface_name)
      return false if iface_name.nil? || iface_name.empty?

      !connections.by_interface(iface_name).empty?
    end

    # @note does not work recursively. So for delete it needs to be called for all modified vlans.
    # @return [ConnectionConfigsCollection] returns collection of interfaces that needs
    #   to be modified or deleted if `connection_config` is deleted or renamed
    def connections_to_modify(connection_config)
      result = []
      bond_bridge = connection_config.find_parent(connections)
      result << bond_bridge if bond_bridge
      vlans = connections.to_a.select do |c|
        c.type.vlan? && c.parent_device == connection_config.name
      end
      result.concat(vlans)
      ConnectionConfigsCollection.new(result)
    end

    # Return whether the config backend is the one given or not
    #
    # @param name [Symbol]
    def backend?(name)
      backend&.id == name
    end

    def backend=(id)
      @backend = Y2Network::Backend.all.find { |b| b.id == id }
    end

    # Updates configuration section
    #
    # This method returns a new instance of Config, leaving the received
    # as it is.
    #
    # @example Update interfaces list
    #   config.update(interfaces: InterfacesCollection.new)
    #
    # @param changes [Hash<Symbol,Object>] A hash where the keys are the
    #   sections to update and the values are the new values
    # @return [Y2Network::Config]
    def update(changes = {})
      self.class.new(
        interfaces:   changes[:interfaces] || interfaces,
        connections:  changes[:connections] || connections,
        s390_devices: changes[:s390_devices] || s390_devices,
        drivers:      changes[:drivers] || drivers,
        routing:      changes[:routing] || routing,
        dns:          changes[:dns] || dns,
        hostname:     changes[:hostname] || hostname,
        source:       changes[:source] || source,
        backend:      changes[:backend] || backend
      )
    end

  private

    def delete_dependents(name)
      connection = connections.by_name(name)

      to_modify = connections_to_modify(connection)
      to_modify.each do |dependency|
        case dependency.type
        when InterfaceType::BRIDGE, InterfaceType::BONDING
          dependency.ports.delete(name)
        when InterfaceType::VLAN
          delete_interface(dependency.interface)
        else
          raise "Unexpected type of interface to modify #{dependency.inspect}"
        end
      end
    end

    def rename_dependencies(old_name, new_name, connection)
      to_modify = connections_to_modify(connection)
      to_modify.each do |dependency|
        case dependency.type
        when InterfaceType::BRIDGE, InterfaceType::BONDING
          dependency.ports.map! { |e| (e == old_name) ? new_name : e }
        when InterfaceType::VLAN
          dependency.parent_device = new_name
        else
          raise "Unexpected type of interface to modify #{dependency.inspect}"
        end
      end
    end
  end
end