Skip to content

Commit

Permalink
Merge pull request #744 from mchf/network-ng
Browse files Browse the repository at this point in the history
Implemented reading of routes via SCR agents (dropped Yast::Routing.Read)
  • Loading branch information
mchf committed Mar 27, 2019
2 parents 7aef386 + 372c19c commit 897214d
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 90 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -31,3 +31,4 @@ testsuite/run/
.yardoc
/coverage
*.pot
config.*
3 changes: 3 additions & 0 deletions src/include/network/lan/complex.rb
Expand Up @@ -144,6 +144,9 @@ def ReadDialog
Wizard.RestoreHelp(Ops.get_string(@help, "read", ""))
Lan.AbortFunction = -> { PollAbort() }
ret = Lan.Read(:cache)
# Currently just a smoketest for new config storage - something what should replace Lan module in the bright future
# TODO: find a suitable place for this config storage
Y2Network::Config.from(:sysconfig)

if Lan.HaveXenBridge
if !Popup.ContinueCancel(
Expand Down
26 changes: 8 additions & 18 deletions src/lib/y2network/config.rb
Expand Up @@ -31,21 +31,20 @@ module Y2Network
# @example Adding a default route to the first routing table
# config = Y2Network::Config.from(:sysconfig)
# route = Y2Network::Route.new(to: :default, interface: :any)
# config.routing_tables.first << route
# config.routing.tables.first << route
# config.write
class Config
# @return [Symbol] Configuration ID
attr_reader :id
# @return [Array<Interface>]
attr_reader :interfaces
# @return [Array<RoutingTable>]
attr_reader :routing_tables
# @return [Routing]
attr_reader :routing
# @return [Symbol] Information source (see {Y2Network::Reader} and {Y2Network::Writer})
attr_reader :source

class << self
# @param source [Symbol] Source to read the configuration from
# @return [Y2Network::Config]
def from(source)
reader = ConfigReader.for(source)
reader.config
Expand All @@ -54,25 +53,16 @@ def from(source)

# Constructor
#
# @param id [Symbol] Configuration ID
# @param interfaces [Array<Interface>] List of interfaces
# @param routing_tables [Array<RoutingTable>] List of routing tables
def initialize(id: :system, interfaces:, routing_tables:, source:)
# @param id [Symbol] Configuration ID
# @param interfaces [Array<Interface>] List of interfaces
# @param routing [Routing] Object with routing configuration
def initialize(id: :system, interfaces:, routing:, source:)
@id = id
@interfaces = interfaces
@routing_tables = routing_tables
@routing = routing
@source = source
end

# Routes in the configuration
#
# Convenience method to iterate through the routes in all routing tables.
#
# @return [Array<Route>] List of routes which are defined in the configuration
def routes
routing_tables.flat_map(&:to_a)
end

# Writes the configuration into the YaST modules
#
# @see Y2Network::ConfigWriter
Expand Down
4 changes: 2 additions & 2 deletions src/lib/y2network/config_reader.rb
Expand Up @@ -19,8 +19,8 @@
module Y2Network
# This module contains a set of classes to read the network configuration from the system
#
# For the time being, only the wicked (through sysconfig files) reader
# ({Y2Network::ConfigReader::Sysconfig}) is available.
# For the time being, only the wicked via its backward compatibility with sysconfig
# is available in ({Y2Network::ConfigReader::Sysconfig}) reader
module ConfigReader
# Config reader for a given source
#
Expand Down
56 changes: 9 additions & 47 deletions src/lib/y2network/config_reader/sysconfig.rb
Expand Up @@ -18,11 +18,12 @@
# find current contact information at www.suse.com.
require "y2network/config"
require "y2network/interface"
require "y2network/routing"
require "y2network/routing_table"
require "y2network/route"
require "y2network/config_reader/sysconfig_routes_reader"

Yast.import "NetworkInterfaces"
Yast.import "Routing"

module Y2Network
module ConfigReader
Expand All @@ -31,16 +32,17 @@ class Sysconfig
# @return [Y2Network::Config] Network configuration
def config
interfaces = find_interfaces
routing_tables = find_routing_tables(interfaces)
Config.new(interfaces: interfaces, routing_tables: routing_tables, source: :sysconfig)
# load /etc/sysconfig/network/routes
routing = Y2Network::Routing.new(tables: [SysconfigRoutesReader.new.config])
# TODO: SysconfigRoutesReader(s) for ifroute-* files
Config.new(interfaces: interfaces, routing: routing, source: :sysconfig)
end

private

MISSING_VALUE = "-".freeze
private_constant :MISSING_VALUE

# Find network interfaces
# Find configured network interfaces
#
# Configured interfaces have a configuration (ifcfg file) assigned.
#
# @return [Array<Interface>] Detected interfaces
# @see Yast::NetworkInterfaces.Read
Expand All @@ -51,46 +53,6 @@ def find_interfaces
Y2Network::Interface.new(name)
end
end

# Find routing tables
#
# @note For the time being, only one routing table is considered.
#
# @param interfaces [Array<Interface>] Detected interfaces
# @return [Array<RoutingTable>]
#
# @see Yast::Routing.Routes
def find_routing_tables(interfaces)
Yast::Routing.Read
routes = Yast::Routing.Routes.map { |h| build_route(interfaces, h) }
table = Y2Network::RoutingTable.new(routes)
[table]
end

# Build a route given a hash from the SCR agent
#
# @param interfaces [Array<Interface>] List of detected interfaces
# @param hash [Hash] Hash from the `.routes` SCR agent
# @return Route
def build_route(interfaces, hash)
iface = interfaces.find { |i| i.name == hash["device"] }
Y2Network::Route.new(
to: hash["destination"] == "default" ? :default : build_ip(hash["destination"], hash["netmask"]),
interface: iface,
gateway: build_ip(hash["gateway"], MISSING_VALUE)
)
end

# Given an IP and a netmask, returns a valid IPAddr object
#
# @param ip_str [String] IP address; {MISSING_VALUE} means that the IP is not defined
# @param netmask_str [String] Netmask; {MISSING_VALUE} means than no netmaks was specified
# @return [IPAddr,nil] The IP address or `nil` if the IP is missing
def build_ip(ip_str, netmask_str = MISSING_VALUE)
return nil if ip_str == MISSING_VALUE
ip = IPAddr.new(ip_str)
netmask_str == MISSING_VALUE ? ip : ip.mask(netmask_str)
end
end
end
end
122 changes: 122 additions & 0 deletions src/lib/y2network/config_reader/sysconfig_routes_reader.rb
@@ -0,0 +1,122 @@
# 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/routing_table"

require "yast"
require "y2network/interface"

module Y2Network
module ConfigReader
DEFAULT_ROUTES_FILE = "/etc/sysconfig/network/routes".freeze

# This class reads the current configuration from a file in routes format
# (@see man routes)
class SysconfigRoutesReader
# @param routes_file [<String>] full path to a file in routes format, when
# not defined, then /etc/sysconfig/network/routes is used
def initialize(routes_file: DEFAULT_ROUTES_FILE)
@routes_file = Yast::Path.new(".routes") if routes_file == DEFAULT_ROUTES_FILE
end

# Load routing tables
#
# Loads content of the routes file specified in the initialization
#
# @return [Y2Network::RoutingTable]
def config
routes = load_routes.map { |r| build_route(r) }
Y2Network::RoutingTable.new(routes)
end

private

MISSING_VALUE = "-".freeze
private_constant :MISSING_VALUE
DEFAULT_DEST = "default".freeze
private_constant :DEFAULT_DEST

# 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_routes
routes = Yast::SCR.Read(@routes_file) || []
normalize_routes(routes.uniq)
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 routes [Array<Hash>] in quad or CIDR flavors (see {#Routes})
# @return [Array<Hash>] in quad or slash flavor
def normalize_routes(routes)
return routes if routes.nil? || routes.empty?

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

next route if prefix.nil?

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

route
end
end

# Given an IP and a netmask, returns a valid IPAddr object
#
# @param ip_str [String] IP address; {MISSING_VALUE} means that the IP is not defined
# @param netmask_str [String] Netmask; {MISSING_VALUE} means than no netmask was specified
# @return [IPAddr,nil] The IP address or `nil` if the IP is missing
def build_ip(ip_str, netmask_str = MISSING_VALUE)
return nil if ip_str == MISSING_VALUE

ip = IPAddr.new(ip_str)
netmask_str == MISSING_VALUE ? ip : ip.mask(netmask_str)
end

# Build a route given a hash from the SCR agent
#
# @param hash [Hash] Hash from the `.routes` SCR agent
# @return Route
def build_route(hash)
# TODO: check whether the iface is configured in the system
iface = Interface.new(hash["device"])
# normalized SCR output contains either subnet mask or /<prefix length> under
# "netmask" key
# TODO: this should be improved in normalize_routes
mask = hash["netmask"] =~ /\/[0-9]+/ ? hash["netmask"][1..-1] : hash["netmask"]

Y2Network::Route.new(
to: hash["destination"] != DEFAULT_DEST ? build_ip(hash["destination"], mask) : :default,
interface: iface,
gateway: build_ip(hash["gateway"], MISSING_VALUE)
)
end
end
end
end
2 changes: 1 addition & 1 deletion src/lib/y2network/config_writer/sysconfig.rb
Expand Up @@ -31,7 +31,7 @@ class Sysconfig
#
# @param config [Y2Network::Config] Configuration to write
def write(config)
routes = config.routes.map { |r| route_to_hash(r) }
routes = config.routing.routes.map { |r| route_to_hash(r) }
Yast::Routing.Import("routes" => routes)
end

Expand Down
42 changes: 42 additions & 0 deletions src/lib/y2network/routing.rb
@@ -0,0 +1,42 @@
# 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.
module Y2Network
# General routing configuration storage (routing tables, forwarding setup, ...)
class Routing
# @return [Array<RoutingTable>]
attr_reader :tables
# @return [Boolean] whether IPv4 forwarding is enabled
attr_reader :forward_ipv4
# @return [Boolean] whether IPv6 forwarding is enabled
attr_reader :forward_ipv6

def initialize(tables:)
@tables = tables
end

# Routes in the configuration
#
# Convenience method to iterate through the routes in all routing tables.
#
# @return [Array<Route>] List of routes which are defined in the configuration
def routes
tables.flat_map(&:to_a)
end
end
end
3 changes: 3 additions & 0 deletions test/data/scr_read/etc/sysconfig/network/routes
@@ -0,0 +1,3 @@
10.29.7.0 10.29.129.1 255.255.255.0 eth0
10.192.0.0/10 10.29.129.1 - -
default 10.29.129.3 -
51 changes: 51 additions & 0 deletions test/y2network/config_reader/sysconfig_routes_reader_test.rb
@@ -0,0 +1,51 @@
# 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_relative "../../test_helper"

require "y2network/config_reader/sysconfig_routes_reader"
require "yast/rspec"

describe Y2Network::ConfigReader::SysconfigRoutesReader do
subject(:reader) { described_class.new }

describe "#config" do
around { |example| change_scr_root("#{DATA_PATH}/scr_read/", &example) }

it "returns a RoutingTable with routes" do
routing_table = reader.config

expect(routing_table).to be_instance_of Y2Network::RoutingTable
expect(routing_table.routes).not_to be_empty
end

it "contains default gw definition" do
expect(reader.config.routes.any?(&:default?)).to be_truthy
end

it "accepts prefix from gateway field" do
route = reader.config.routes.find { |r| r.to == "10.192.0.0" }

expect(route.to.prefix).to eql 10
end

it "stores device when set" do
expect(reader.config.routes.any? { |r| r.interface.name == "eth0" }).to be_truthy
end
end
end

0 comments on commit 897214d

Please sign in to comment.