From 940b15a159b28b3a2c3b151fbf8d3d06ca637b30 Mon Sep 17 00:00:00 2001 From: Philippe PITTOLI Date: Thu, 3 Oct 2019 16:52:25 +0200 Subject: [PATCH] networkctl: first draft --- .gitignore | 5 + shard.yml | 27 +++++ src/colors.cr | 5 + src/main.cr | 328 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 365 insertions(+) create mode 100644 .gitignore create mode 100644 shard.yml create mode 100644 src/colors.cr create mode 100644 src/main.cr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b945dcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +shard.lock +src/.* +src/.*swp +lib/ diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..fb8b5ea --- /dev/null +++ b/shard.yml @@ -0,0 +1,27 @@ +name: networkctl +version: 0.1.0 + +# authors: +# - name + +description: | + networkctl is a command-line software to manage the network. + +# dependencies: +# pg: +# github: will/crystal-pg +# version: "~> 0.5" + +targets: + networkctl: + main: src/main.cr + +dependencies: + ipaddress: + github: sija/ipaddress.cr + +# development_dependencies: +# webmock: +# github: manastech/webmock.cr + +license: ISC diff --git a/src/colors.cr b/src/colors.cr new file mode 100644 index 0000000..4c4d49c --- /dev/null +++ b/src/colors.cr @@ -0,0 +1,5 @@ +CRED = "\033[31m" +CBLUE = "\033[36m" +CGREEN = "\033[32m" +CRESET = "\033[00m" +CORANGE = "\033[33m" diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..8e04f1f --- /dev/null +++ b/src/main.cr @@ -0,0 +1,328 @@ + +require "option_parser" +require "ipaddress" +require "./colors" + +simulation = false +file = nil + +OptionParser.parse! do |parser| + parser.on "-s", "--simulation", "Export the network configuration." do + simulation = true + end + + parser.on "-f file", "--file file", "Parse a configuration file." do |optsn| + file = optsn + end + + # 0: nothing is printed, 1: only events, 2: events and messages + parser.on "-v verbosity", "--verbosity verbosity", "Verbosity (0-2). Default: 1" do |optsn| + verbosity = optsn.to_i + end + + parser.on "-h", "--help", "Show this help" do + puts parser + exit 0 + end +end + +class Do < Process + class_property simulation = false + + def self.run(cmd : String, params : Array(String) = nil) + if @@simulation + puts "simulation, do: #{cmd} #{params.join(" ")}" + Process::Status.new 0 + else + Process.run cmd, params + end + end +end + + +class NetworkCommands + class DHCPCommands + def self.dhcp(ifname : String) + # TODO: verify which dhcp client is installed on the system + cmd = "udhcpc" + unless Do.run(cmd, [ name ]).success? + raise "(#{cmd}) dhcp failed on #{ifname}" + end + end + end + + def self.which(cmd : String) + Do.run("which", [ cmd ]).success? + end + + class IfconfigCommand + def self.interface_exists?(name : String) + Do.run("ifconfig", [ name ]).success? + end + def self.up_or_down(name : String, updown : String) + unless Do.run("ifconfig", [ name, updown ]).success? + raise "(ifconfig) Cannot set #{updown} link name #{name}" + end + end + def self.up(name : String) + self.up_or_down name, "up" + end + def self.down(name : String) + self.up_or_down name, "down" + end + + def self.set_ip(name : String, ip : IPAddress) + puts "(ip) setup static IP address" + end + end + + class IPCommand + def self.interface_exists?(name : String) + Do.run("ip", [ "link", "show", "dev", name ]).success? + end + def self.up_or_down(name : String, updown : String) + unless Do.run("ip", [ "link", "set", updown, "dev", name ]).success? + raise "(ip) Cannot set #{updown} link name #{name}" + end + end + def self.up(name : String) + self.up_or_down name, "up" + end + def self.down(name : String) + self.up_or_down name, "down" + end + + def self.set_ip(name : String, ip : IPAddress) + puts "(ip) setup static IP address" + end + end + + + def self.choose_command : IfconfigCommand.class | IPCommand.class + if self.which("ifconfig") + IfconfigCommand + elsif self.which("ip") + IPCommand + else + raise "Neither ifconfig or ip commands exists on this system" + end + end + + def self.interface_exists?(name : String) + cmd = self.choose_command + cmd.interface_exists?(name) + end + + def self.up(ifname : String) + cmd = self.choose_command + cmd.up(ifname) + end + + def self.down(ifname : String) + cmd = self.choose_command + cmd.up(ifname) + end + + def self.set_ip(name : String, ip : IPAddress) + puts "(ip) setup static IP address" + end + + def self.dhcp(name : String) + puts "(ip) setup dynamic IP address" + DHCPCommands.dhcp name + end +end + + +# +# interface configuration +# + +class InterfaceConfiguration + class NotSetup + def to_s(io : IO) + io << "not setup" + end + end + + class DHCP + def to_s(io : IO) + io << "dhcp" + end + end + + property name : String + property up : Bool + property main_ip_v4 : IPAddress | DHCP | NotSetup + property main_ip_v6 : IPAddress | DHCP | NotSetup + property aliasses_v4 : Array(IPAddress) + property aliasses_v6 : Array(IPAddress) + + def initialize (@name, @up, @main_ip_v4, @main_ip_v6, aliasses) + @aliasses_v4 = Array(IPAddress).new + @aliasses_v6 = Array(IPAddress).new + + aliasses.each do |ip| + if ip.ipv4? + @aliasses_v4 << ip + else + @aliasses_v6 << ip + end + end + end + + def to_s(io : IO) + io << to_string + end + + def to_string + String.build do |str| + if NetworkCommands.interface_exists?(@name) + str << "#{CGREEN}#{@name}#{CRESET}\n" + else + str << "#{CRED}#{@name}#{CRESET}\n" + end + + str << "\t#{@up? "up" : "down"}\n" + str << "\tinet #{@main_ip_v4}\n" + + unless @aliasses_v4.empty? + @aliasses_v4.each do |a| + str << "\talias #{a}\n" + end + end + + str << "\tinet6 #{@main_ip_v6}\n" + + unless @aliasses_v6.empty? + @aliasses_v6.each do |a| + str << "\talias6 #{a}\n" + end + end + end + end + + # configure the interface + def execute + unless NetworkCommands.interface_exists?(@name) + raise "The interface #{@name} doesn't exists, yet." + end + + puts "OK on va configurer ça" + puts "#{self}" + + if @up + NetworkCommands.up @name + else + puts "not marked as 'up' -- ending here" + return + end + + + case main_ip_v4 = @main_ip_v4 + when IPAddress + NetworkCommands.set_ip @name, main_ip_v4 + when DHCP + NetworkCommands.dhcp @name + when NotSetup + puts "no ipv4" + else + raise "ipv4 configuration: neither static nor dynamic" + end + + case main_ip_v6 = @main_ip_v6 + when IPAddress + NetworkCommands.set_ip @name, main_ip_v6 + # TODO + #when Autoconfiguration + # NetworkCommands.autoconfiguration @name + #when DHCP + # NetworkCommands.dhcp6 @name + when NotSetup + puts "no ipv4" + else + raise "ipv4 configuration: neither static nor dynamic" + end + + + # str << "\tinet #{@main_ip_v4}\n" + + # str << "\t#{@up? "up" : "down"}\n" + # str << "\tinet #{@main_ip_v4}\n" + + # unless @aliasses_v4.empty? + # @aliasses_v4.each do |a| + # str << "\talias #{a}\n" + # end + # end + + # str << "\tinet6 #{@main_ip_v6}\n" + + # @aliasses_v6.each do |a| + # str << "\talias6 #{a}\n" + # end + # alias execution: only when the main ip address is setup + end +end + +class NetworkConfigurationParser + def self.parse_file(file_name : String) : InterfaceConfiguration + content = File.read(file_name) + content = content.rchop + ifname = /.([a-zA-Z0-9]+)$/.match(file_name).try &.[1] + self.parse(ifname.not_nil!, content) + end + + def self.parse (ifname : String, data : String) : InterfaceConfiguration + up = false + main_ip_v4 = InterfaceConfiguration::NotSetup.new + main_ip_v6 = InterfaceConfiguration::NotSetup.new + + aliasses = [] of IPAddress + + data.split("\n").each do |line| + case line + when /^up/ + up = true + when /^inet6? alias .*/ + ipstr = /^inet6? alias ([a-f0-9:.\/]+)/.match(line).try &.[1] + if ipstr.nil? + puts "wrong IP address alias, line #{line}" + next + end + aliasses.push IPAddress.parse(ipstr) + when /^inet6? dhcp/ + # IPaddress is DHCP + if /^inet /.match(line) + main_ip_v4 = InterfaceConfiguration::DHCP.new + else + main_ip_v6 = InterfaceConfiguration::DHCP.new + end + when /^inet6? .*/ + ipstr = /^inet6? ([a-f0-9:.\/]+)/.match(line).try &.[1] + if ipstr.nil? + puts "wrong IP address, line #{line}" + next + end + if /^inet /.match(line) + main_ip_v4 = IPAddress.parse ipstr + else + main_ip_v6 = IPAddress.parse ipstr + end + else + raise "Cannot parse: #{line}" + end + end + + InterfaceConfiguration.new(ifname, up, main_ip_v4, main_ip_v6, aliasses) + end +end + +Do.simulation = simulation + +if file.nil? + raise "Cannot choose files yet" +else + # TODO: why having to force "not_nil!" ? Seems like a compiler bug + NetworkConfigurationParser.parse_file(file.not_nil!).execute +end