src/modules/Lan.rb
# ***************************************************************************
#
# Copyright (c) 2012 Novell, Inc.
# 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 Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# **************************************************************************
# File: modules/Lan.ycp
# Package: Network configuration
# Summary: Network card data
# Authors: Michal Svec <msvec@suse.cz>
#
#
# Representation of the configuration of network cards.
# Input and output routines.
require "yast"
require "cfa/sysctl_config"
require "network/network_autoyast"
require "network/confirm_virt_proposal"
require "network/wicked"
require "ui/text_helpers"
require "y2firewall/firewalld"
require "y2network/autoinst_profile/networking_section"
require "y2network/autoinst/config"
require "y2network/config"
require "y2network/virtualization_config"
require "y2network/interface_config_builder"
require "y2network/presenters/summary"
require "y2network/interface_type"
require "y2network/proposal_settings"
require "y2issues"
require "shellwords"
Yast.import "HTML"
Yast.import "Summary"
module Yast
class LanClass < Module
include ::UI::TextHelpers
include Wicked
def main
Yast.import "UI"
textdomain "network"
Yast.import "Arch"
Yast.import "NetHwDetection"
Yast.import "Host"
Yast.import "IP"
Yast.import "Map"
Yast.import "Mode"
Yast.import "NetworkConfig"
Yast.import "NetworkService"
Yast.import "Package"
Yast.import "ProductFeatures"
Yast.import "Progress"
Yast.import "String"
Yast.import "FileUtils"
Yast.import "Package"
Yast.import "ModuleLoading"
Yast.import "Linuxrc"
Yast.include self, "network/complex.rb"
Yast.include self, "network/runtime.rb"
#-------------
# GLOBAL DATA
# gui or cli mode
@gui = true
@write_only = false
# ipv6 module
@ipv6 = true
# Hotplug type ("" if not hot pluggable)
# Abort function
# return boolean return true if abort
@AbortFunction = nil
# Lan::Read (`cache) will do nothing if initialized already.
@initialized = false
@backend = nil
@modified = false
# Y2Network::Config objects
@configs = {}
end
# @return [Boolean]
attr_accessor :write_only
# @return [Autoinst::Config]
attr_writer :autoinst
#------------------
# GLOBAL FUNCTIONS
#------------------
# Return a modification status
# @return true if data was modified
def Modified
return true if @modified
return true unless system_config == yast_config
return true if NetworkConfig.Modified
return true if NetworkService.Modified
return true if Host.GetModified
false
end
def SetModified
@modified = true
nil
end
# function for use from autoinstallation (Fate #301032)
def isAnyInterfaceDown
down = false
link_status = devices_link_status
log.info("link_status #{link_status}")
configurations = (Yast::Lan.system_config&.interfaces&.map(&:name) || [])
configurations.each do |devname|
address_file = "/sys/class/net/#{devname}/address"
mac = ::File.read(address_file).chomp if ::File.file?(address_file)
log.info("confname #{mac}")
if !link_status.keys.include?(mac)
log.error("Mac address #{mac} not found in map #{link_status}!")
elsif link_status[mac] == false
log.warn("Interface with mac #{mac} is down!")
down = true
else
log.debug("Interface with mac #{mac} is up")
end
end
down
end
# Checks local configuration if IPv6 is allowed
#
# return [Boolean] true when IPv6 is enabled in the system
def readIPv6
sysctl_config_file = CFA::SysctlConfig.new
sysctl_config_file.load
ipv6 = !sysctl_config_file.disable_ipv6
log.info("readIPv6: IPv6 is #{ipv6 ? "enabled" : "disabled"}")
ipv6
end
def read_step_labels
steps = [
# Progress stage 1/5
_("Detect network devices"),
# Progress stage 2/5
_("Read driver information"),
# Progress stage 3/5
_("Detect current status"),
# Progress stage 4/5 - multiple devices may be present, really plural
_("Read device configuration"),
# Progress stage 5/5
_("Read network configuration")
]
steps << _("Read firewall configuration") if firewalld.installed?
steps
end
# Read all network settings from the SCR
# @param cache [Symbol] :cache=use cached data, :nocache=reread from disk
# TODO: pass to submodules
# @return true on success
def Read(cache)
if cache == :cache && @initialized
Builtins.y2milestone("Using cached data")
return true
end
# Read dialog caption
caption = _("Initializing Network Configuration")
sl = 0 # 1000; /* TESTING
Builtins.sleep(sl)
if @gui
Progress.New(
caption,
" ",
read_step_labels.size,
read_step_labels,
[],
""
)
end
return false if Abort()
ProgressNextStage(_("Detecting ndiswrapper...")) if @gui
# modprobe ndiswrapper before hwinfo when needed (#343893)
if !Mode.autoinst && Package.Installed("ndiswrapper")
Builtins.y2milestone("ndiswrapper: installed")
if Ops.greater_than(
Builtins.size(
Convert.convert(
SCR.Read(path(".target.dir"), "/etc/ndiswrapper"),
from: "any",
to: "list <string>"
)
),
0
)
Builtins.y2milestone("ndiswrapper: configuration found")
if Convert.to_integer(
SCR.Execute(path(".target.bash"), "/usr/sbin/lsmod | /usr/bin/grep -q ndiswrapper")
) != 0 &&
Popup.YesNo(
_(
"Detected a ndiswrapper configuration,\n" \
"but the kernel module was not modprobed.\n" \
"Do you want to modprobe ndiswrapper?\n"
)
) && (ModuleLoading.Load("ndiswrapper", "", "", "", false, true) == :fail)
Popup.Error(
_(
"ndiswrapper kernel module has not been loaded.\nCheck configuration manually.\n"
)
)
end
end
end
Builtins.sleep(sl)
return false if Abort()
ProgressNextStage(_("Detecting network devices...")) if @gui
# Dont read hardware data in config mode
NetHwDetection.Start if !Mode.config
Builtins.sleep(sl)
return false if Abort()
begin
ProgressNextStage(_("Detecting current status...")) if @gui
NetworkService.Read
Builtins.sleep(sl)
return false if Abort()
ProgressNextStage(_("Reading device configuration...")) if @gui
return false unless read_config
Builtins.sleep(sl)
return false if Abort()
ProgressNextStage(_("Reading network configuration...")) if @gui
NetworkConfig.Read
@ipv6 = readIPv6
Builtins.sleep(sl)
Host.Read
Builtins.sleep(sl)
return false if Abort()
if firewalld.installed? && !firewalld.read?
ProgressNextStage(_("Reading firewall configuration...")) if @gui
firewalld.read
Builtins.sleep(sl)
end
return false if Abort()
rescue IOError, SystemCallError, RuntimeError => e
msg = format(_("Network configuration is corrupted.\n"\
"If you continue resulting configuration can be malformed."\
"\n\n%s"), wrap_text(e.message))
return false if !@gui
return false if !Popup.ContinueCancel(msg)
end
# Final progress step
ProgressNextStage(_("Finished")) if @gui
Builtins.sleep(sl)
return false if Abort()
@initialized = true
fix_dhclient_warning(invalid_dhcp_cfgs) if @gui && !valid_dhcp_cfg?
Progress.Finish if @gui
true
end
# Creates a list of config files which contain corrupted DHCLIENT_SET_HOSTNAME setup
#
# @return [Array] list of config file names
def invalid_dhcp_cfgs
devs = find_set_hostname_ifaces
dev_ifcfgs = devs.map { |d| "ifcfg-#{d}" }
return dev_ifcfgs if devs.size > 1
return dev_ifcfgs << "dhcp" if !devs.empty? && DNS.dhcp_hostname
[]
end
# Checks if system DHCLIENT_SET_HOSTNAME is valid
#
# @return [Boolean]
def valid_dhcp_cfg?
invalid_dhcp_cfgs.empty?
end
# Finds all devices which has DHCLIENT_SET_HOSTNAME set to "yes"
#
# @return [Array<String>] list of NIC names which has the option set to "yes"
def find_set_hostname_ifaces
yast_config.connections.map do |conn|
conn.interface if conn.dhclient_set_hostname == "yes"
end.compact
end
# (a specialization used when a parameterless function is needed)
# @return [Boolean] true on success
def ReadWithCache
Read(:cache)
end
def ReadWithCacheNoGUI
@gui = false
ReadWithCache()
end
def SetIPv6(status)
if @ipv6 != status
@ipv6 = status
Popup.Warning(_("To apply this change, a reboot is needed."))
Lan.SetModified
end
nil
end
def writeIPv6
log.info("writeIPv6: IPv6 is #{@ipv6 ? "enabled" : "disabled"}")
sysctl_config_file = CFA::SysctlConfig.new
sysctl_config_file.load
sysctl_config_file.disable_ipv6 = !@ipv6
sysctl_config_file.save unless sysctl_config_file.conflict?
SCR.Execute(
path(".target.bash"),
Builtins.sformat(
"/usr/sbin/sysctl -w net.ipv6.conf.all.disable_ipv6=%1",
@ipv6 ? "0" : "1"
)
)
SCR.Write(
path(".sysconfig.windowmanager.KDE_USE_IPV6"),
@ipv6 ? "yes" : "no"
)
nil
end
# Update the SCR according to network settings
# @return true on success
def Write(gui: true, apply_config: !write_only)
log.info("Writing configuration")
# Query modified flag in all components
if !Modified()
log.info("No changes to network setup -> nothing to write")
return true
end
# Write dialog caption
caption = _("Saving Network Configuration")
sl = 0 # 1000; /* TESTING
Builtins.sleep(sl)
step_labels = [
# Progress stage 2
_("Write drivers information"),
# Progress stage 3 - multiple devices may be present,really plural
_("Write device configuration"),
# Progress stage 4
_("Write network configuration"),
# Progress stage 5
_("Write routing configuration"),
# Progress stage 6
_("Write hostname and DNS configuration"),
# Progress stage 7
_("Set up network services")
]
# Progress stage 8
step_labels << _("Writing firewall configuration") if firewalld.installed?
# Progress stage 9
step_labels = Builtins.add(step_labels, _("Activate network services")) if apply_config
# Progress stage 10
step_labels = Builtins.add(step_labels, _("Update configuration"))
Progress.New(
caption,
" ",
Builtins.size(step_labels),
step_labels,
[],
""
)
return false if Abort()
# Progress step 2
ProgressNextStage(_("Writing /etc/modprobe.conf..."))
Builtins.sleep(sl)
return false if Abort()
# Progress step 3 - multiple devices may be present, really plural
ProgressNextStage(_("Writing device configuration..."))
Builtins.sleep(sl)
return false if Abort()
# Progress step 4
ProgressNextStage(_("Writing network configuration..."))
NetworkConfig.Write
Builtins.sleep(sl)
return false if Abort()
# Progress step 5
ProgressNextStage(_("Writing routing configuration..."))
orig = Progress.set(false)
write_config
Progress.set(orig)
Builtins.sleep(sl)
return false if Abort()
# Progress step 6
ProgressNextStage(_("Writing hostname and DNS configuration..."))
# write resolv.conf after change from dhcp to static (#327074)
# reload/restart network before this to put correct resolv.conf from dhcp-backup
orig = Progress.set(false)
Host.Write(gui: gui)
Progress.set(orig)
Builtins.sleep(sl)
return false if Abort()
# Progress step 7
ProgressNextStage(_("Setting up network services..."))
writeIPv6
Builtins.sleep(sl)
if firewalld.installed?
return false if Abort()
# Progress step 7
ProgressNextStage(_("Writing firewall configuration..."))
firewalld.write
Builtins.sleep(sl)
end
if apply_config
return false if Abort()
# Progress step 9
ProgressNextStage(_("Activating network services..."))
select_network_service ? activate_network_service : NetworkService.disable
Builtins.sleep(sl)
end
return false if Abort()
# Progress step 10
ProgressNextStage(_("Updating configuration..."))
update_mta_config if !apply_config
Builtins.sleep(sl)
ensure_network_running if yast_config.backend?(:network_manager)
# Final progress step
ProgressNextStage(_("Finished"))
Builtins.sleep(sl)
Progress.Finish
return false if Abort()
true
end
# Only write configuration without starting any init scripts and SuSEconfig
# @return true on success
def WriteOnly
@write_only = !autoinst.start_immediately
Write()
end
def autoinst
@autoinst ||= Y2Network::Autoinst::Config.new
end
# If there's key in modify, upcase key and assign the value to ret
# @return ret
def UpcaseCondSet(ret, modify, key)
ret = deep_copy(ret)
modify = deep_copy(modify)
Ops.set(ret, Builtins.toupper(key), Ops.get(modify, key)) if Builtins.haskey(modify, key)
deep_copy(ret)
end
# Convert data from autoyast to structure used by module.
# @param [Hash] input autoyast settings
# @return [Hash] native network settings
# FIXME: massive refactoring required
def FromAY(input)
input = deep_copy(input)
Builtins.y2debug("input %1", input)
input["interfaces"] ||= []
# DHCP:: config: some of it is in the DNS part of the profile
dhcp = {}
dhcpopts = Ops.get_map(input, "dhcp_options", {})
dns = Ops.get_map(input, "dns", {})
if Builtins.haskey(dns, "dhcp_hostname")
Ops.set(
dhcp,
"DHCLIENT_SET_HOSTNAME",
Ops.get_boolean(dns, "dhcp_hostname", false)
)
end
dhcp = UpcaseCondSet(dhcp, dhcpopts, "dhclient_client_id")
dhcp = UpcaseCondSet(dhcp, dhcpopts, "dhclient_additional_options")
dhcp = UpcaseCondSet(dhcp, dhcpopts, "dhclient_hostname_option")
Ops.set(input, "config", "dhcp" => dhcp)
if !Ops.get(input, "strict_IP_check_timeout").nil?
input["strict_ip_check_timeout"] = input.delete("strict_IP_check_timeout")
Ops.set(input, ["config", "config"], "CHECK_DUPLICATE_IP" => true)
end
Builtins.y2milestone("input=%1", input)
input
end
# Import data.
# It expects data described networking.rnc
# and then passed through {#FromAY}.
# Most prominently, instead of a flat list called "interfaces"
# we import a 2-level map of typed "devices"
# @param [Hash] settings settings to be imported
# @return true on success
def Import(settings)
settings = {} if settings.nil?
if Arch.s390
NetworkAutoYast.instance.activate_s390_devices(settings.fetch("s390-devices", {}))
end
Read(:cache)
profile = Y2Network::AutoinstProfile::NetworkingSection.new_from_hashes(settings)
result = Y2Network::Config.from(:autoinst, profile, system_config)
add_config(:yast, result.config)
@autoinst = autoinst_config(profile)
NetworkConfig.Import(settings["config"] || {})
# Ensure that the /etc/hosts has been read to no blank out it in case of
# not defined <host> section (bsc#1058396)
Host.Read
@ipv6 = settings.fetch("ipv6", true)
true
end
# Export data.
# They need to be converted to become what networking.rnc describes.
# @see Y2Network::Clients::Auto.adapt_for_autoyast
#
# Most prominently, instead of a flat list called "interfaces"
# we export a 2-level map of typed "devices"
# @return dumped settings
def Export
profile = Y2Network::AutoinstProfile::NetworkingSection.new_from_network(yast_config)
ay = profile.to_hashes
ay["ipv6"] = @ipv6
ay["config"] = NetworkConfig.Export unless ay["managed"]
log.info("Exported map: #{ay}")
deep_copy(ay)
end
# Create a textual summary and a list of unconfigured devices
# @param [String] mode "summary": add resolver and routing summary,
# "proposal": for proposal also with resolver an routing summary
# @return summary of the current configuration
def Summary(mode)
return no_config_message if yast_config.nil?
case mode
when "summary", "proposal"
Y2Network::Presenters::Summary.text_for(yast_config, "proposal")
else
Y2Network::Presenters::Summary.text_for(yast_config, "interfaces")
end
end
def ProposeVirtualized
read_config unless yast_config
Y2Network::VirtualizationConfig.new(yast_config).create
end
# Proposes additional packages when needed by current networking setup
#
# @return [Array] of packages needed when writing the config
def Packages
backend = if Mode.autoinst
Y2Network::Backend.all.find { |b| b.id == proposal_settings.current_backend }
else
yast_config&.backend
end
return [] unless backend
target = Mode.autoinst ? :autoinst : :system
needed_packages = backend.packages.reject { |p| Package.Installed(p, target: target) }
if (backend.id == :wicked) && !Package.Installed("wpa_supplicant", target: target)
# we have to add wpa_supplicant when wlan is in game, wicked relies on it
wlan_present = yast_config.interfaces.any? do |iface|
iface.type == Y2Network::InterfaceType::WIRELESS
end
needed_packages << "wpa_supplicant" if wlan_present
end
needed_packages.uniq!
unless needed_packages.empty?
log.info("Additional packages requested by yast2-network: #{needed_packages.inspect}")
end
needed_packages
end
# @return [Array] of packages needed when writing the config in autoinst
# mode
def AutoPackages
{ "install" => Packages(), "remove" => [] }
end
# Xen bridging confuses us (#178848)
# @return whether xenbr* exists
def HaveXenBridge
# adapted test for xen bridged network (bnc#553794)
have_br = FileUtils.Exists("/dev/.sysconfig/network/xenbridges")
Builtins.y2milestone("Have Xen bridge: %1", have_br)
have_br
end
# Provides a list with the NTP servers obtained via any of dhcp aware
# interfaces
#
# @note parsing dhcp ntp servers when NetworkManager is in use is not
# supported yet (bsc#798886)
# @note called from installer
#
# @return [Array<String>] list of ntp servers obtained byg DHCP
def dhcp_ntp_servers
# Used before the config has been read
return [] if !NetworkService.isNetworkRunning || Yast::NetworkService.is_network_manager
ReadWithCacheNoGUI()
ifaces_dhcp_ntp_servers.values.flatten.uniq
end
# Returns hash of NTP servers
#
# Provides map with NTP servers obtained via any of dhcp aware interfaces
#
# @return [Hash<String, Array<String>] key is device name, value
# is list of ntp servers obtained from the device
def ifaces_dhcp_ntp_servers
dhcp_ifaces = yast_config.connections.select(&:dhcp?).map(&:interface).compact
result = dhcp_ifaces.map { |iface| [iface, parse_ntp_servers(iface)] }.to_h
result.delete_if { |_, ntps| ntps.empty? }
end
# Returns the network configuration with the given ID
#
# @param id [Symbol] Network configuration ID
# @return [Y2Network::Config,nil] Network configuration with the given ID or nil if not found
def find_config(id)
Y2Network::Config.find(id)
end
# Adds the configuration
#
# @param id [Symbol] Configuration ID
# @param config [Y2Network::Config] Network configuration
def add_config(id, config)
Y2Network::Config.add(id, config)
end
# Clears the network configurations list
def clear_configs
Y2Network::Config.reset
end
# Returns the system configuration
#
# Just a convenience method.
#
# @return [Y2Network::Config]
def system_config
find_config(:system)
end
# Returns YaST configuration
#
# Just a convenience method.
#
# @return [Y2Network::Config]
def yast_config
find_config(:yast)
end
# Reads system configuration
#
# It resets the already read configuration. If some issue is detected
# while reading the configuration, it is reported to the user.
#
# @param report [Boolean] Report any found issue if set to true
# @return [Boolean] Returns whether the configuration was read.
def read_config(report: true)
result = Y2Network::Config.from(:wicked)
return false if result.issues? && report && !Y2Issues.report(result.issues)
system_config = result.config
system_config.backend = (NetworkService.cached_name || :none)
Yast::Lan.add_config(:system, system_config)
Yast::Lan.add_config(:yast, system_config.copy)
true
end
# Writes current yast config and replaces the system config with it
#
# @param only [Array<Symbol>, nil] explicit sections to be written, by default if no
# parameter is given then all changes will be written
#
# @see Y2Network::ConfigWriter
def write_config(only: nil, report: true)
result = yast_config.write(original: system_config, only: only)
return false if result&.issues? && report && !Y2Issues.report(result.issues)
# Force a refresh of the system_config bsc#1162987
add_config(:system, yast_config.copy)
end
publish variable: :ipv6, type: "boolean"
publish variable: :AbortFunction, type: "block <boolean>"
publish function: :Modified, type: "boolean ()"
publish function: :isAnyInterfaceDown, type: "boolean ()"
publish function: :Read, type: "boolean (symbol)"
publish function: :ReadWithCache, type: "boolean ()"
publish function: :ReadWithCacheNoGUI, type: "boolean ()"
publish function: :SetIPv6, type: "void (boolean)"
publish function: :Write, type: "boolean ()"
publish function: :WriteOnly, type: "boolean ()"
publish function: :Import, type: "boolean (map)"
publish function: :Export, type: "map ()"
publish function: :Summary, type: "list (string)"
publish function: :Add, type: "boolean ()"
publish function: :Delete, type: "boolean ()"
publish function: :AnyDHCPDevice, type: "boolean ()"
publish function: :Packages, type: "list <string> ()"
publish function: :AutoPackages, type: "map ()"
publish function: :HaveXenBridge, type: "boolean ()"
NM_DHCP_TIMEOUT = 45
private
def ensure_network_running
network = false
timeout = NM_DHCP_TIMEOUT
while timeout > 0
if NetworkService.isNetworkRunning
network = true
break
end
log.info("waiting for network ... #{timeout}")
sleep(1)
timeout -= 1
end
Popup.Error(_("No network running")) unless network
end
# @return [Array<Y2Network::Config>]
attr_reader :configs
def autoinst_config(section)
ay_settings = autoinst_settings(section)
return if ay_settings.empty?
Y2Network::Autoinst::Config.new(ay_settings)
end
def proposal_settings
Y2Network::ProposalSettings.instance
end
def autoinst_settings(section)
{
before_proposal: section.setup_before_proposal,
start_immediately: section.start_immediately,
keep_install_network: section.keep_install_network,
ip_check_timeout: section.strict_ip_check_timeout,
virt_bridge_proposal: section.virt_bridge_proposal,
managed: section.managed,
backend: section.backend
}.compact
end
def activate_network_service
# If the second installation stage has been called by yast.ssh via
# ssh, we should not restart network because systemctl
# hangs in that case. (bnc#885640)
action = :reload_restart if Stage.normal || !Linuxrc.usessh
action = :remote_installer if Stage.initial && (Linuxrc.usessh || Linuxrc.vnc)
case action
when :reload_restart
log.info("Attempting to reload network service, normal stage #{Stage.normal}, " \
"ssh: #{Linuxrc.usessh}")
NetworkService.ReloadOrRestart if Stage.normal || !Linuxrc.usessh
when :remote_installer
connection_names = yast_config&.connections&.map(&:name) || []
# last instance handling "special" cases like ssh installation
# FIXME: most probably not everything will be set properly
log.info("Running in ssh/vnc installer -> just setting links up")
log.info("Configured connections: #{connection_names}")
reload_config(connection_names)
end
end
def select_network_service
# We cannot switch the service during installation
return true if Stage.initial
NetworkService.use(yast_config&.backend&.id)
end
def firewalld
Y2Firewall::Firewalld.instance
end
# Obtains the list of the system network devices names through the sysfs
#
# @return [Array<String>] names of the system network devices
def devices_from_sys
interfaces = Yast::Execute.stdout.on_target!("/usr/bin/ls", "/sys/class/net").split("\n")
interfaces.delete("lo")
interfaces
end
# Returns the mac address and whether the device has an ip associated or
# not of the given interface name
#
# @return [Array(2)<String, Boolean>] mac address and ip assignation status
def link_status(name)
addr_show = ["/usr/sbin/ip", "address", "show", name]
inet_link = ["grep", "inet\\|link"]
row = Yast::Execute.stdout.on_target!(addr_show, inet_link).split("\n").map(&:strip)
addr = false
tmp_mac = nil
row.each do |column|
tmp_col = column.split
next if tmp_col.size < 2
tmp_mac = tmp_col[1] if tmp_col[0] == "link/ether"
addr = true if tmp_col[0] == "inet"
end
[tmp_mac, addr]
end
# Convenience method to obtain the mac and ip assignment status of the
# system network devices
def devices_link_status
devices_from_sys.each_with_object({}) do |name, hash|
tmp_mac, addr = link_status(name)
hash[tmp_mac] ||= addr if tmp_mac
log.debug("link_status #{hash.inspect}")
end
end
def no_config_message
HTML.Heading(_("Network Configuration")) + Summary.NotConfigured
end
end
Lan = LanClass.new
Lan.main
end