diff --git a/src/lib/cfa/nm_connection.rb b/src/lib/cfa/nm_connection.rb index 5c00df90f..9cc100e70 100644 --- a/src/lib/cfa/nm_connection.rb +++ b/src/lib/cfa/nm_connection.rb @@ -22,28 +22,68 @@ module CFA # Class to handle NetworkManager connection configuration files # + # @see https://developer.gnome.org/NetworkManager/stable/nm-settings-keyfile.html # @example Reading the connection name # file = NmConnection.new("/etc/NetworkManager/system-connections/eth0.nmconnection") # file.load # puts file.connection["id"] class NmConnection < BaseModel + KNOWN_SECTIONS = [ + "bridge", "connection", "ethernet", "ipv4", "ipv6", "vlan", "wifi", "wifi_security" + ].freeze + + # @return [String] file path + attr_reader :path + # Constructor # # @param path [String] File path # @param file_handler [.read, .write] Object to read/write the file. def initialize(path, file_handler: nil) - super(AugeasParser.new("Networkmanager.lns"), path, file_handler: file_handler) + @path = path + # FIXME: The Networkmanager lense writes the values surrounded by double + # quotes which is not valid + super(AugeasParser.new("Puppet.lns"), @path, file_handler: file_handler) end - # Returns the augeas tree for the connection section + # Returns the augeas tree for the given section # - # If the "[connection]" section does not exist, it returns an empty one. + # If the given section does not exist, it returns an empty one # + # @param name [String] section name # @return [AugeasTree] - def connection - return data["connection"] if data["connection"] + def section_for(name) + sname = name.gsub("_", "-") + return data[sname] if data[sname] + + data[sname] ||= CFA::AugeasTree.new + end + + # Sets an array's property under an specific section + # + # Array properties are written as a numbered variable. This method takes + # care of numbering them according to the variable name. + # + # @example Write IPv4 addresses + # ipv4_addresses = ["192.168.1.100/24", "192.168.20.200/32"] + # file.add_collection("ipv4", "address", ipv4_addresses) + # + # # Writes: + # # [ipv4] + # # address1="192.168.1.100/24" + # # address2="192.168.20.200/32" + # + # @param section [String] section name + # @param name [String] variable name to be used + # @param values [Array] variable values + def add_collection(section, name, values) + section = section_for(section) - data["connection"] ||= CFA::AugeasTree.new + values.each_with_index do |ip, index| + section["#{name}#{index + 1}"] = ip + end end + + KNOWN_SECTIONS.each { |s| define_method(s) { section_for(s) } } end end diff --git a/src/lib/y2network/network_manager/connection_config_writer.rb b/src/lib/y2network/network_manager/connection_config_writer.rb index 42cabb790..ddb762f71 100644 --- a/src/lib/y2network/network_manager/connection_config_writer.rb +++ b/src/lib/y2network/network_manager/connection_config_writer.rb @@ -31,16 +31,24 @@ class ConnectionConfigWriter def write(conn, old_conn = nil) return if conn == old_conn - file = CFA::NmConnection.new(SYSTEM_CONNECTIONS_PATH.join(conn.name).sub_ext(FILE_EXT)) + path = SYSTEM_CONNECTIONS_PATH.join(conn.name).sub_ext(FILE_EXT) + file = CFA::NmConnection.new(path) handler_class = find_handler_class(conn.type) return nil if handler_class.nil? + ensure_permissions(path) unless ::File.exist?(path) + handler_class.new(file).write(conn) file.save end private + def ensure_permissions(path) + ::FileUtils.touch(path) + ::File.chmod(0o600, path) + end + # Returns the class to handle a given interface type # # @param type [Y2Network::InterfaceType] Interface type diff --git a/src/lib/y2network/network_manager/connection_config_writers/base.rb b/src/lib/y2network/network_manager/connection_config_writers/base.rb index 4341028ee..1b6274f85 100644 --- a/src/lib/y2network/network_manager/connection_config_writers/base.rb +++ b/src/lib/y2network/network_manager/connection_config_writers/base.rb @@ -17,6 +17,9 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "y2network/boot_protocol" +require "securerandom" + module Y2Network module NetworkManager module ConnectionConfigWriters @@ -39,11 +42,45 @@ def initialize(file) # @param conn [Y2Network::ConnectionConfig::Base] Connection to take settings from def write(conn) file.connection["id"] = conn.name + file.connection["autoconnect"] = "false" if ["manual", "off"].include? conn.startmode.name + file.connection["permissions"] = nil + file.connection["interface-name"] = conn.interface + file.connection["zone"] = conn.firewall_zone unless ["", nil].include? conn.firewall_zone + conn.bootproto.dhcp? ? configure_dhcp(conn) : add_ips(conn) update_file(conn) end private + # FIXME: Gateway is missing + # Convenience method for writing the static IP configuration + # + # @param conn [Y2Network::ConnectionConfig::Base] Connection to take settings from + def add_ips(conn) + ips_to_add = conn.ip_aliases.clone + ips_to_add.append(conn.ip) if conn.ip + ipv4 = ips_to_add.select { |i| i&.address&.ipv4? }.map { |i| i.address.to_s } + ipv6 = ips_to_add.select { |i| i&.address&.ipv6? }.map { |i| i.address.to_s } + + unless ipv4.empty? + file.ipv4["method"] = "manual" + file.add_collection("ipv4", "address", ipv4) + end + + return if ipv6.empty? + + file.ipv6["method"] = "manual" + file.add_collection("ipv6", "address", ipv6) + end + + # Convenience method for writing the DHCP config + # + # @param conn [Y2Network::ConnectionConfig::Base] Connection to take settings from + def configure_dhcp(conn) + file.ipv4["method"] = "auto" if conn.bootproto != Y2Network::BootProtocol::DHCP6 + file.ipv6["method"] = "auto" if conn.bootproto != Y2Network::BootProtocol::DHCP4 + end + # Sets file values from the given connection configuration # # @note This method should be redefined by derived classes. diff --git a/src/lib/y2network/network_manager/connection_config_writers/ethernet.rb b/src/lib/y2network/network_manager/connection_config_writers/ethernet.rb index f4e273567..a325f977c 100644 --- a/src/lib/y2network/network_manager/connection_config_writers/ethernet.rb +++ b/src/lib/y2network/network_manager/connection_config_writers/ethernet.rb @@ -26,7 +26,9 @@ module ConnectionConfigWriters # object to the underlying system. class Ethernet < Base # @see Y2Network::ConnectionConfigWriters::Base#update_file - def update_file(_conn); end + def update_file(_conn) + file.connection["type"] = "ethernet" + end end end end diff --git a/src/lib/y2network/network_manager/connection_config_writers/vlan.rb b/src/lib/y2network/network_manager/connection_config_writers/vlan.rb new file mode 100644 index 000000000..cc5bc8de3 --- /dev/null +++ b/src/lib/y2network/network_manager/connection_config_writers/vlan.rb @@ -0,0 +1,38 @@ +# Copyright (c) [2021] 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/network_manager/connection_config_writers/base" + +module Y2Network + module NetworkManager + module ConnectionConfigWriters + # This class is responsible for writing the information from a ConnectionConfig::Vlan + # object to the underlying system. + class Vlan < Base + # @see Y2Network::ConnectionConfigWriters::Base#update_file + # @param conn [Y2Network::ConnectionConfig::Vlan] Configuration to write + def update_file(conn) + file.vlan["id"] = conn.vlan_id + file.vlan["parent"] = conn.parent_device + file.vlan["type"] = "vlan" + end + end + end + end +end diff --git a/src/lib/y2network/network_manager/connection_config_writers/wireless.rb b/src/lib/y2network/network_manager/connection_config_writers/wireless.rb new file mode 100644 index 000000000..3d66b0e0d --- /dev/null +++ b/src/lib/y2network/network_manager/connection_config_writers/wireless.rb @@ -0,0 +1,83 @@ +# Copyright (c) [2021] 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/network_manager/connection_config_writers/base" + +module Y2Network + module NetworkManager + module ConnectionConfigWriters + # This class is responsible for writing the information from a ConnectionConfig::Wireless + # object to the underlying system. + class Wireless < Base + MODE = { "ad-hoc" => "ad-hoc", "master" => "ap", "managed" => "infrastructure" }.freeze + + # @see Y2Network::ConnectionConfigWriters::Base#update_file + def update_file(conn) + file.connection["type"] = "wifi" + file.wifi["ssid"] = conn.essid unless conn.essid.to_s.empty? + file.wifi["mode"] = MODE[conn.mode] + file.wifi["channel"] = con.channel if conn.channel + + write_auth_settings(conn) + end + + # Writes autentication settings for WPA-EAP networks + # + # @param _conn [Y2Network::ConnectionConfig::Base] Configuration to write + def write_eap_auth_settings(_conn) + # FIXME: incomplete + file.wifi_security["key-mgmt"] = "wpa-eap" + # wrong section name + # + # file.802_1x["eap"] = conn.eap_mode + # file.802_1x["phase2-auth"] = conn.eap_auth + end + + # Writes authentication settings + # + # This method relies in `write_*_auth_settings` methods. + # + # + # @param conn [Y2Network::ConnectionConfig::Base] Configuration to write + # + # @see #write_eap_auth_settings + # @see #write_psk_auth_settings + # @see #write_shared_auth_settings + def write_auth_settings(conn) + meth = "write_#{conn.auth_mode}_auth_settings".to_sym + send(meth, conn) if respond_to?(meth, true) + end + + # Writes autentication settings for WPA-PSK networks + # + # @param conn [Y2Network::ConnectionConfig::Base] Configuration to write + def write_psk_auth_settings(conn) + file.wifi_security["auth-alg"] = "open" + file.wifi_security["key-mgmt"] = "wpa-psk" + file.wifi_security["psk"] = conn.wpa_psk + end + + # Writes autentication settings for WEP networks + # + # @param _conn [Y2Network::ConnectionConfig::Base] Configuration to write + def write_shared_auth_settings(_conn); end + end + end + end +end diff --git a/test/y2network/network_manager/connection_config_writer_test.rb b/test/y2network/network_manager/connection_config_writer_test.rb index 215b44963..5e68bc846 100644 --- a/test/y2network/network_manager/connection_config_writer_test.rb +++ b/test/y2network/network_manager/connection_config_writer_test.rb @@ -51,8 +51,10 @@ Y2Network::ConnectionConfig::IPConfig.new(Y2Network::IPAddress.from_string("10.100.0.1/24")) end + let(:path) { "/etc/NetworkManager/system-connections/eth0.nmconnection" } + let(:file) do - instance_double(CFA::NmConnection, save: nil) + instance_double(CFA::NmConnection, save: nil, path: path) end describe "#write" do @@ -67,6 +69,8 @@ allow(Y2Network::NetworkManager::ConnectionConfigWriters::Ethernet).to receive(:new) .and_return(handler) allow(CFA::NmConnection).to receive(:new).and_return(file) + allow(writer).to receive(:ensure_permissions) + allow(::File).to receive(:exist?).with(Pathname.new(path)).and_return(true) end it "uses the appropiate handler" do @@ -75,6 +79,17 @@ writer.write(conn) end + context "when the file does not exist" do + before do + allow(::File).to receive(:exist?).with(Pathname.new(path)).and_return(false) + end + + it "ensures the file is created with the the correct permissions" do + expect(writer).to receive(:ensure_permissions).with(Pathname.new(path)) + writer.write(conn) + end + end + it "does nothing if the connection has not changed" end end diff --git a/test/y2network/network_manager/connection_config_writers/wireless_test.rb b/test/y2network/network_manager/connection_config_writers/wireless_test.rb new file mode 100644 index 000000000..e0930a261 --- /dev/null +++ b/test/y2network/network_manager/connection_config_writers/wireless_test.rb @@ -0,0 +1,72 @@ +# 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/network_manager/connection_config_writers/wireless" +require "cfa/nm_connection" +require "y2network/boot_protocol" +require "y2network/startmode" +require "y2network/connection_config/wireless" + +describe Y2Network::NetworkManager::ConnectionConfigWriters::Wireless do + subject(:handler) { described_class.new(file) } + let(:file) { CFA::NmConnection.new("wlan0.nm_connection") } + + let(:conn) do + Y2Network::ConnectionConfig::Wireless.new.tap do |c| + c.interface = "wlan0" + c.description = "Wireless Card 0" + c.startmode = Y2Network::Startmode.create("auto") + c.bootproto = Y2Network::BootProtocol::DHCP + c.mode = "managed" + c.essid = "example_essid" + c.auth_mode = :open + c.ap = "00:11:22:33:44:55" + c.ap_scanmode = "1" + end + end + + describe "#write" do + it "sets relevant attributes" do + handler.write(conn) + expect(file.wifi["ssid"]).to eql(conn.essid) + expect(file.wifi["mode"]).to eql("infrastructure") + expect(file.ipv4["method"]).to eql("auto") + expect(file.ipv6["method"]).to eql("auto") + end + + context "WPA-PSK network configuration" do + let(:conn) do + Y2Network::ConnectionConfig::Wireless.new.tap do |c| + c.startmode = Y2Network::Startmode.create("auto") + c.bootproto = Y2Network::BootProtocol::DHCP + c.mode = "managed" + c.auth_mode = "psk" + c.wpa_psk = "example_psk" + end + end + + it "sets specific WPA-PSK attributes" do + handler.write(conn) + expect(file.wifi_security["key-mgmt"]).to eql("wpa-psk") + expect(file.wifi_security["psk"]).to eql("example_psk") + end + end + end +end