commit 940b15a159b28b3a2c3b151fbf8d3d06ca637b30
Author: Philippe PITTOLI
Date: Thu Oct 3 16:52:25 2019 +0200
networkctl: first draft
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