networkctl: first draft
This commit is contained in:
commit
940b15a159
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
bin/
|
||||
shard.lock
|
||||
src/.*
|
||||
src/.*swp
|
||||
lib/
|
27
shard.yml
Normal file
27
shard.yml
Normal file
@ -0,0 +1,27 @@
|
||||
name: networkctl
|
||||
version: 0.1.0
|
||||
|
||||
# authors:
|
||||
# - name <email@example.com>
|
||||
|
||||
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
|
5
src/colors.cr
Normal file
5
src/colors.cr
Normal file
@ -0,0 +1,5 @@
|
||||
CRED = "\033[31m"
|
||||
CBLUE = "\033[36m"
|
||||
CGREEN = "\033[32m"
|
||||
CRESET = "\033[00m"
|
||||
CORANGE = "\033[33m"
|
328
src/main.cr
Normal file
328
src/main.cr
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user