(Very alpha!) Add dpkg builder and initial configurator

* Adds a new Rust program, `lqos_setup`.
    * If no /etc/lqos.conf is found, prompts for interfaces and
      creates a dual-interface XDP bridge setup.
    * If no /opt/libreqos/src/ispConfig.py is found, prompts
      for bandwidth and creates one (using the interfaces also)
    * Same for ShapedDevices.csv and network.json
    * If no webusers are found, prompts to make one.
* Adds build_dbpkg.sh
    * Creates a new directory named `dist`
    * Builds the Rust components in a portable mode.
    * Creates a list of dependencies and DEBIAN directory
      with control and postinst files.
    * Handles PIP dependencies in postinst
    * Calls the new `lqos_setup` program for final
      configuration.
    * Sets up the daemons in systemd and enables them.

In very brief testing, I had a working XDP bridge with
1 fake user and a total bandwidth limit configured and
working after running:

dpkg -i 1.4-1.dpkg
apt -f install

Could still use some tweaking.
This commit is contained in:
Herbert Wolverson 2023-02-21 20:26:34 +00:00
parent e74662631c
commit 3e9ff0c0f5
6 changed files with 367 additions and 0 deletions

1
.gitignore vendored
View File

@ -52,6 +52,7 @@ src/lastRun.txt
src/liblqos_python.so
src/webusers.toml
src/lqusers.toml
src/dist
# Ignore Rust build artifacts
src/rust/target

113
src/build_dpkg.sh Executable file
View File

@ -0,0 +1,113 @@
#!/bin/bash
####################################################
# Copyright (c) 2022, Herbert Wolverson and LibreQoE
# This is all GPL2.
PACKAGE=libreqos
VERSION=1.4
DPKG_DIR=dist/$PACKAGE_$VERSION-1_amd64
APT_DEPENDENCIES="python3-pip, clang, gcc, gcc-multilib, llvm, libelf-dev, git, nano, graphviz, curl, screen, llvm, pkg-config, linux-tools-common, libbpf-dev"
DEBIAN_DIR=$DPKG_DIR/DEBIAN
LQOS_DIR=$DPKG_DIR/opt/libreqos/src
ETC_DIR=$DPKG_DIR/etc
LQOS_FILES="graphInfluxDB.py influxDBdashboardTemplate.json integrationCommon.py integrationRestHttp.py integrationSplynx.py integrationUISP.py ispConfig.example.py LibreQoS.py lqos.example lqTools.py mikrotikFindIPv6.py network.example.json pythonCheck.py README.md scheduler.py ShapedDevices.example.csv"
LQOS_BIN_FILES="lqos_scheduler.service.example lqosd.service.example lqos_node_manager.service.example"
RUSTPROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_setup"
####################################################
# Clean any previous dist build
rm -rf dist
####################################################
# The Debian Packaging Bit
# Create the basic directory structure
mkdir -p $DEBIAN_DIR
# Build the chroot directory structure
mkdir -p $LQOS_DIR
mkdir -p $LQOS_DIR/bin/static
mkdir -p $ETC_DIR
# Create the Debian control file
pushd $DEBIAN_DIR > /dev/null
touch control
echo "Package: $PACKAGE" >> control
echo "Version: $VERSION" >> control
echo "Architecture: amd64" >> control
echo "Maintainer: Herbert Wolverson <herberticus@gmail.com>" >> control
echo "Description: CAKE-based traffic shaping for ISPs" >> control
echo "Depends: $APT_DEPENDENCIES" >> control
popd > /dev/null
# Create the post-installation file
pushd $DEBIAN_DIR > /dev/null
touch postinst
echo "#!/bin/bash" >> postinst
echo "# Install Python Dependencies" >> postinst
echo "pushd /opt/libreqos" >> postinst
# - Setup Python dependencies as a post-install task
while requirement= read -r line
do
echo "python3 -m pip install $line" >> postinst
echo "sudo python3 -m pip install $line" >> postinst
done < ../../../../requirements.txt
# - Run lqsetup
echo "/opt/libreqos/src/bin/lqos_setup" >> postinst
# - Setup the services
echo "cp /opt/libreqos/src/bin/lqos_node_manager.service.example /etc/systemd/system/lqos_node_manager.service" >> postinst
echo "cp /opt/libreqos/src/bin/lqosd.service.example /etc/systemd/system/lqosd.service" >> postinst
echo "cp /opt/libreqos/src/bin/lqos_scheduler.service.example /etc/systemd/system/lqos_scheduler.service" >> postinst
echo "/bin/systemctl daemon-reload" >> postinst
echo "/bin/systemctl enable lqosd lqos_node_manager lqos_scheduler" >> postinst
echo "/bin/systemctl start lqosd" >> postinst
echo "/bin/systemctl start lqos_node_manager" >> postinst
echo "/bin/systemctl start lqos_scheduler" >> postinst
echo "popd" >> postinst
chmod a+x postinst
popd > /dev/null
# Create the cleanup file
pushd $DEBIAN_DIR > /dev/null
touch postrm
echo "#!/bin/bash" >> postrm
chmod a+x postrm
popd > /dev/null
# Copy files into the LibreQoS directory
for file in $LQOS_FILES
do
cp $file $LQOS_DIR
done
# Copy files into the LibreQoS/bin directory
for file in $LQOS_BIN_FILES
do
cp bin/$file $LQOS_DIR/bin
done
####################################################
# Build the Rust programs
pushd rust > /dev/null
cargo clean
cargo build --all --release
popd > /dev/null
# Copy newly built Rust files
# - The Python integration Library
cp rust/target/release/liblqos_python.so $LQOS_DIR
# - The main executables
for prog in $RUSTPROGS
do
cp rust/target/release/$prog $LQOS_DIR/bin
done
# - The webserver skeleton files
cp rust/lqos_node_manager/Rocket.toml $LQOS_DIR/bin
cp -R rust/lqos_node_manager/static/* $LQOS_DIR/bin/static
####################################################
# Assemble the package
pushd dist / dev/null
dpkg-deb --root-owner-group --build $DPKG_DIR
popd > /dev/null

19
src/rust/Cargo.lock generated
View File

@ -399,6 +399,17 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "colored"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
dependencies = [
"atty",
"lazy_static",
"winapi",
]
[[package]]
name = "cookie"
version = "0.16.2"
@ -1402,6 +1413,14 @@ dependencies = [
name = "lqos_rs"
version = "0.1.0"
[[package]]
name = "lqos_setup"
version = "0.1.0"
dependencies = [
"colored",
"default-net",
]
[[package]]
name = "lqos_sys"
version = "0.1.0"

View File

@ -22,4 +22,5 @@ members = [
"lqos_python", # Python bindings for using the Rust bus directly
"lqusers", # CLI control for managing the web user list
"lqos_utils", # A collection of macros and helpers we find useful
"lqos_setup", # A quick CLI setup for first-time users
]

View File

@ -0,0 +1,8 @@
[package]
name = "lqos_setup"
version = "0.1.0"
edition = "2021"
[dependencies]
colored = "2"
default-net = "0" # For obtaining an easy-to-use NIC list

View File

@ -0,0 +1,225 @@
use std::{path::Path, fs, process::Command};
use colored::Colorize;
use default_net::{get_interfaces, interface::InterfaceType, Interface};
fn get_available_interfaces() -> Vec<Interface> {
get_interfaces()
.iter()
.filter(|eth| eth.if_type == InterfaceType::Ethernet && !eth.name.starts_with("br"))
.cloned()
.collect()
}
fn should_build(path: &str) -> bool {
if Path::new(path).exists() {
let string = format!("Skipping: {path}");
println!("{}", string.red());
println!("{}", "You already have one installed\n".cyan());
return false;
}
true
}
fn list_interfaces(interfaces: &Vec<Interface>) {
println!("{}", "Available Interfaces".white());
for i in interfaces {
let iftype = format!("{:?}", i.if_type);
println!("{} - {}", i.name.cyan(), iftype.yellow());
}
}
fn is_valid_interface(interfaces: &[Interface], iface: &str) -> bool {
interfaces.iter().any(|i| i.name == iface)
}
pub fn read_line() -> String {
let mut guess = String::new();
std::io::stdin()
.read_line(&mut guess)
.expect("failed to readline");
guess.trim().to_string()
}
pub fn read_line_as_number() -> u32 {
loop {
let str = read_line();
if let Ok(n) = str::parse::<u32>(&str) {
return n;
}
println!("Could not parse [{str}] as a number. Try again.");
}
}
const LQOS_CONF: &str = "/etc/lqos.conf";
const ISP_CONF: &str = "/opt/libreqos/src/ispConfig.py";
const NETWORK_JSON: &str = "/opt/libreqos/src/network.json";
const SHAPED_DEVICES: &str = "/opt/libreqos/src/ShapedDevices.csv";
const LQUSERS: &str = "/opt/libreqos/src/lqusers.toml";
fn get_internet_interface(interfaces: &Vec<Interface>, if_internet: &mut Option<String>) {
if if_internet.is_none() {
println!("{}", "Which Network Interface faces the INTERNET?".yellow());
list_interfaces(interfaces);
loop {
let iface = read_line();
if is_valid_interface(interfaces, &iface) {
*if_internet = Some(iface);
break;
} else {
println!("{}", "Not a valid interface".red());
}
}
}
}
fn get_isp_interface(interfaces: &Vec<Interface>, if_isp: &mut Option<String>) {
if if_isp.is_none() {
println!("{}", "Which Network Interface faces the ISP CORE?".yellow());
list_interfaces(interfaces);
loop {
let iface = read_line();
if is_valid_interface(interfaces, &iface) {
*if_isp = Some(iface);
break;
} else {
println!("{}", "Not a valid interface".red());
}
}
}
}
fn get_bandwidth(up: bool) -> u32 {
loop {
match up {
true => println!("{}", "How much UPLOAD bandwidth do you have? (Mbps, e.g. 1000 = 1 gbit)".yellow()),
false => println!("{}", "How much DOWNLOAD bandwidth do you have? (Mbps, e.g. 1000 = 1 gbit)".yellow()),
}
let bandwidth = read_line_as_number();
if bandwidth > 0 {
return bandwidth;
}
}
}
const ETC_LQOS_CONF: &str = "lqos_directory = '/opt/libreqos/src'
queue_check_period_ms = 1000
[tuning]
stop_irq_balance = true
netdev_budget_usecs = 8000
netdev_budget_packets = 300
rx_usecs = 8
tx_usecs = 8
disable_rxvlan = true
disable_txvlan = true
disable_offload = [ \"gso\", \"tso\", \"lro\", \"sg\", \"gro\" ]
[bridge]
use_xdp_bridge = true
interface_mapping = [
{ name = \"{INTERNET}\", redirect_to = \"{ISP}\", scan_vlans = false },
{ name = \"{ISP}\", redirect_to = \"{INTERNET}\", scan_vlans = false }
]
vlan_mapping = []
";
fn write_etc_lqos_conf(internet: &str, isp: &str) {
let output = ETC_LQOS_CONF.replace("{INTERNET}", internet).replace("{ISP}", isp);
fs::write(LQOS_CONF, output).expect("Unable to write file");
}
pub fn write_isp_config_py(
dir: &str,
download: u32,
upload: u32,
lan: &str,
internet: &str,
) {
// Copy ispConfig.example.py to ispConfig.py
let orig = format!("{dir}ispConfig.example.py");
let dest = format!("{dir}ispConfig.py");
std::fs::copy(orig, &dest).unwrap();
let config_file = std::fs::read_to_string(&dest).unwrap();
let mut new_config_file = String::new();
config_file.split('\n').for_each(|line| {
if line.contains("upstreamBandwidthCapacityDownloadMbps") {
new_config_file += &format!("upstreamBandwidthCapacityDownloadMbps = {download}\n");
} else if line.contains("upstreamBandwidthCapacityUploadMbps") {
new_config_file += &format!("upstreamBandwidthCapacityUploadMbps = {upload}\n");
} else if line.contains("interfaceA") {
new_config_file += &format!("interfaceA = \"{lan}\"\n");
} else if line.contains("interfaceB") {
new_config_file += &format!("interfaceB = \"{internet}\"\n");
} else if line.contains("generatedPNDownloadMbps") {
new_config_file += &format!("generatedPNDownloadMbps = \"{download}\"\n");
} else if line.contains("generatedPNUploadMbps") {
new_config_file += &format!("generatedPNUploadMbps = \"{upload}\"\n");
} else {
new_config_file += line;
new_config_file += "\n";
}
});
std::fs::write(&dest, new_config_file).unwrap();
}
fn write_network_json() {
let output = "{}\n";
fs::write(NETWORK_JSON, output).expect("Unable to write file");
}
const EMPTY_SHAPED_DEVICES: &str = "# This is a comment
Circuit ID,Circuit Name,Device ID,Device Name,Parent Node,MAC,IPv4,IPv6,Download Min Mbps,Upload Min Mbps,Download Max Mbps,Upload Max Mbps,Comment
# This is another comment
\"9999\",\"968 Circle St., Gurnee, IL 60031\",1,Device 1,AP_7,,\"100.64.1.2, 100.64.0.14\",,25,5,500,500,";
fn write_shaped_devices() {
fs::write(SHAPED_DEVICES, EMPTY_SHAPED_DEVICES).expect("Unable to write file");
}
fn main() {
println!("{:^80}", "LibreQoS 1.4 Setup Assistant".yellow().on_blue());
println!();
let interfaces = get_available_interfaces();
let mut if_internet: Option<String> = None;
let mut if_isp: Option<String> = None;
if should_build(LQOS_CONF) {
println!("{}{}", LQOS_CONF.cyan(), "does not exist, building one.".white());
get_internet_interface(&interfaces, &mut if_internet);
get_isp_interface(&interfaces, &mut if_isp);
if let (Some(internet), Some(isp)) = (&if_internet, &if_isp) {
write_etc_lqos_conf(internet, isp);
}
}
if should_build(ISP_CONF) {
println!("{}{}", ISP_CONF.cyan(), "does not exist, building one.".white());
get_internet_interface(&interfaces, &mut if_internet);
get_isp_interface(&interfaces, &mut if_isp);
let upload = get_bandwidth(true);
let download = get_bandwidth(false);
if let (Some(internet), Some(isp)) = (&if_internet, &if_isp) {
write_isp_config_py("/opt/libreqos/src/", download, upload, isp, internet)
}
}
if should_build(NETWORK_JSON) {
println!("{}{}", NETWORK_JSON.cyan(), "does not exist, making a simple flat network.".white());
write_network_json();
}
if should_build(SHAPED_DEVICES) {
println!("{}{}", SHAPED_DEVICES.cyan(), "does not exist, making an empty one.".white());
println!("{}", "Don't forget to add some users!".magenta());
write_shaped_devices();
}
if should_build(LQUSERS) {
println!("Enter a username for the web manager:");
let user = read_line();
println!("Enter a password for the web manager:");
let password = read_line();
Command::new("/opt/libreqos/src/bin/lqusers")
.args(["add", "--username", &user, "--role", "admin", "--password", &password])
.output()
.unwrap();
}
}