yast/yast-network

View on GitHub
src/lib/cfa/routes_file.rb

Summary

Maintainability
A
0 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 "yast"
require "yast2/execute"
require "y2network/interface"
require "y2network/route"
require "y2network/serializer/route_sysconfig"
Yast.import "FileUtils"

module CFA
  # This class represents a file containing a set of routes
  #
  # @example Reading the default location, i.e., /etc/sysconfig/network/routes
  #   file = CFA::RoutesFile.new
  #   file.load
  #   file.routes.size #=> 2
  #
  # @example Reading routes for the interface eth1
  #   file = CFA::RoutesFile.new("/etc/sysconfig/network/ifroute-eth1")
  #   file.load
  #   file.routes.size #=> 1
  class RoutesFile
    DEFAULT_ROUTES_FILE = "/etc/sysconfig/network/routes".freeze
    SYSCONFIG_NETWORK_DIR = "/etc/sysconfig/network".freeze

    # @return [Array<Route>] Routes
    attr_accessor :routes

    # @return [String] File path
    attr_reader :file_path

    class << self
      # @param interface [String] Interface's name
      def find(interface)
        file_path = File.join(SYSCONFIG_NETWORK_DIR, "ifroute-#{interface}")
        new(file_path)
      end
    end

    # @param file_path [String] File path
    def initialize(file_path = DEFAULT_ROUTES_FILE)
      @register_agent = file_path != DEFAULT_ROUTES_FILE
      @file_path = file_path
    end

    # Loads routes from system
    #
    # @return [Array<Hash<String, String>>] list of hashes representing routes
    #   as provided by SCR agent.
    #   keys: destination, gateway, netmask, [device, [extrapara]]
    def load
      entries = with_registered_ifroute_agent(file_path) { |a| Yast::SCR.Read(a) }
      entries = entries ? normalize_entries(entries.uniq) : []
      @routes = entries.map { |r| serializer.from_hash(r) }
    end

    # Writes configured routes
    #
    # @return [Boolean] true on success
    def save
      # create if not exists, otherwise backup
      if Yast::FileUtils.Exists(file_path)
        Yast::Execute.on_target("/bin/cp", file_path, file_path + ".YaST2save")
      end

      with_registered_ifroute_agent(file_path) do |scr|
        # work around bnc#19476
        Yast::SCR.Write(Yast::Path.new(".target.string"), file_path, "")
        Yast::SCR.Write(scr, routes.map { |r| serializer.to_hash(r) })
      end
    end

    # Removes the file
    def remove
      return unless Yast::FileUtils.Exists(file_path)

      Yast::SCR.Execute(Yast::Path.new(".target.remove"), file_path)
    end

  private

    # Convenience method to obtain a new route to hash serializer
    #
    # @return [Y2Network::Serializer::RouteSysconfig]
    def serializer
      @serializer ||= Y2Network::Serializer::RouteSysconfig.new
    end

    # Converts routes config as read from system into well-defined format
    #
    # Expects list of hashes as param. Hash should contain keys "destination",
    # "gateway", "netmask", "device", "extrapara"
    #
    # Currently it converts "destination" in CIDR format (<ip>/<prefix_len>)
    # and keeps just <ip> part in "destination" and puts "/<prefix_len>" into
    # "netmask"
    #
    # @param entries [Array<Hash>] in quad or CIDR flavors (see {#Routes})
    # @return [Array<Hash>] in quad or slash flavor
    def normalize_entries(entries)
      return entries if entries.nil? || entries.empty?

      entries.map do |entry|
        subnet, prefix = entry["destination"].split("/")

        next entry if prefix.nil?

        entry["destination"] = subnet
        entry["netmask"] = "/#{prefix}"

        entry
      end
    end

    NON_EMPTY_STR_TERM = Yast.term(:String, "^ \t\n").freeze
    WHITESPACE_TERM = Yast.term(:Whitespace).freeze
    OPTIONAL_WHITESPACE_TERM = Yast.term(:Optional, WHITESPACE_TERM).freeze
    ROUTES_CONTENT_TERM = Yast.term(
      :List,
      Yast.term(
        :Tuple,
        Yast.term(
          :destination,
          NON_EMPTY_STR_TERM
        ),
        WHITESPACE_TERM,
        Yast.term(:gateway, NON_EMPTY_STR_TERM),
        WHITESPACE_TERM,
        Yast.term(:netmask, NON_EMPTY_STR_TERM),
        OPTIONAL_WHITESPACE_TERM,
        Yast.term(
          :Optional,
          Yast.term(:device, NON_EMPTY_STR_TERM)
        ),
        OPTIONAL_WHITESPACE_TERM,
        Yast.term(
          :Optional,
          Yast.term(
            :extrapara,
            Yast.term(:String, "^\n")
          )
        )
      ),
      "\n"
    ).freeze

    # SCR agent for routes files definition
    def ifroute_term(path)
      raise ArgumentError if path.nil? || path.empty?

      Yast.term(
        :ag_anyagent,
        Yast.term(
          :Description,
          Yast.term(:File, path),
          "#\n", # TODO: document these arguments
          false,
          ROUTES_CONTENT_TERM
        )
      )
    end

    # Executes a block of passing the ifroute agent as a parameter
    #
    # @param file_path [String] Path to the routes file
    # @param block     [Proc] Code to execute
    # @return [Object] Returns the value of the block
    def with_registered_ifroute_agent(file_path, &block)
      scr_path = ifroute_agent_scr_path(file_path)
      block.call(scr_path)
    ensure
      # scr_path can failed when creating agent, so in such case, do not unregister it
      Yast::SCR.UnregisterAgent(scr_path) if register_agent? && scr_path
    end

    # Returns the path to the SCR agent
    #
    # If needed, it registers the agent
    #
    # @return [Yast::Path]
    def ifroute_agent_scr_path(file_path)
      return Yast::Path.new(".routes") unless register_agent?

      register_ifroute_agent_for_path(file_path)
    end

    # Determines whether the agent should be registered on the fly
    #
    # @return [Boolean] true if the agent needs to be registered
    def register_agent?
      @register_agent
    end

    # Registers SCR agent which is used for accessing particular ifroute-device
    # file
    #
    # @param file_path [String] full path to a file in routes format
    #   (e.g. /etc/sysconfig/network/ifroute-eth0)
    # @return [Yast::Path] SCR path of the agent
    # @raise  [RuntimeError] if it fails
    def register_ifroute_agent_for_path(file_path)
      # /etc/sysconfig/network/ifroute-eth0 define .ifroute-eth0 agent
      # TODO: collisions not handled
      scr_path = Yast::Path.new(".#{File.basename(file_path)}")
      Yast::SCR.RegisterAgent(scr_path, ifroute_term(file_path)) ||
        raise("Cannot register agent (#{scr_path})")
      scr_path
    end
  end
end