File nm-configurator-0.3.3.obscpio of Package nm-configurator
07070100000000000081A40000000000000000000000016835937A00000007000000000000000000000000000000000000002400000000nm-configurator-0.3.3/.dockerignoretarget/07070100000001000041ED0000000000000000000000026835937A00000000000000000000000000000000000000000000001E00000000nm-configurator-0.3.3/.github07070100000002000081A40000000000000000000000016835937A000000D3000000000000000000000000000000000000002D00000000nm-configurator-0.3.3/.github/dependabot.yml---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
07070100000003000041ED0000000000000000000000026835937A00000000000000000000000000000000000000000000002800000000nm-configurator-0.3.3/.github/workflows07070100000004000081A40000000000000000000000016835937A0000024B000000000000000000000000000000000000003B00000000nm-configurator-0.3.3/.github/workflows/build_and_test.ymlon:
pull_request:
push:
branches:
- "*"
name: Build & Test
env:
CARGO_TERM_COLOR: always
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Format
run: cargo fmt --all -- --check
- name: Lint
run: cargo clippy --all-targets --all-features -- -D warnings
build:
runs-on: ubuntu-latest
needs: [ lint ]
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build
- name: Test
run: cargo test --no-fail-fast
env:
RUST_LOG: debug
07070100000005000081A40000000000000000000000016835937A000003ED000000000000000000000000000000000000003400000000nm-configurator-0.3.3/.github/workflows/release.ymlon:
push:
tags:
- 'v*'
name: Release
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build x86_64 binary
run: |
docker build -t nmc:amd64 --platform linux/amd64 .
container_id=$(docker create nmc:amd64 --entrypoint /)
docker cp $container_id:/target/release/nmc nmc-linux-x86_64
- name: Build aarch64 binary
run: |
docker build -t nmc:arm64 --platform linux/arm64 .
container_id=$(docker create nmc:arm64 --entrypoint /)
docker cp $container_id:/target/release/nmc nmc-linux-aarch64
- name: Release
uses: softprops/action-gh-release@v2
with:
files: |
nmc-linux-aarch64
nmc-linux-x86_64
07070100000006000081A40000000000000000000000016835937A000000E1000000000000000000000000000000000000002100000000nm-configurator-0.3.3/.gitignore# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
07070100000007000081A40000000000000000000000016835937A000094C3000000000000000000000000000000000000002100000000nm-configurator-0.3.3/Cargo.lock# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "configparser"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b"
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "endi"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enumflags2"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "ethtool"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5479e15c34374ec0240622c15f2152647ba8726bc6e15f33335a83e309f7b1a5"
dependencies = [
"anyhow",
"byteorder",
"futures",
"genetlink",
"log",
"netlink-packet-core",
"netlink-packet-generic",
"netlink-packet-utils",
"netlink-proto",
"netlink-sys",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "event-listener"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "genetlink"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f890076c1faa1298bf747ce3694a8d9e0d2cc4b06fe293f12dd95742bfd079f"
dependencies = [
"futures",
"log",
"netlink-packet-core",
"netlink-packet-generic",
"netlink-packet-utils",
"netlink-proto",
"thiserror 1.0.69",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "hashbrown"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
[[package]]
name = "mptcp-pm"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eafa8fc63dce407b75e336f9a22f18cf5510a3a5c3a5d83262688eb5cca42d5"
dependencies = [
"anyhow",
"byteorder",
"futures",
"genetlink",
"log",
"netlink-packet-core",
"netlink-packet-generic",
"netlink-packet-utils",
"netlink-proto",
"netlink-sys",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "netlink-packet-core"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4"
dependencies = [
"anyhow",
"byteorder",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-generic"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd7eb8ad331c84c6b8cb7f685b448133e5ad82e1ffd5acafac374af4a5a308b"
dependencies = [
"anyhow",
"byteorder",
"netlink-packet-core",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-route"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"byteorder",
"libc",
"log",
"netlink-packet-core",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-utils"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34"
dependencies = [
"anyhow",
"byteorder",
"paste",
"thiserror 1.0.69",
]
[[package]]
name = "netlink-proto"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60"
dependencies = [
"bytes",
"futures",
"log",
"netlink-packet-core",
"netlink-sys",
"thiserror 2.0.12",
]
[[package]]
name = "netlink-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23"
dependencies = [
"bytes",
"futures",
"libc",
"log",
"tokio",
]
[[package]]
name = "network-interface"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3329f515506e4a2de3aa6e07027a6758e22e0f0e8eaf64fa47261cec2282602"
dependencies = [
"cc",
"libc",
"thiserror 1.0.69",
"winapi",
]
[[package]]
name = "nispor"
version = "1.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de4f8494aa978fe693dc7f36cdfcac99f1f1441f71c44267278b889e36d52c1a"
dependencies = [
"ethtool",
"futures",
"libc",
"log",
"mptcp-pm",
"rtnetlink",
"serde",
"serde_json",
"tokio",
"wl-nl80211",
]
[[package]]
name = "nix"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]]
name = "nmc"
version = "0.3.3"
dependencies = [
"anyhow",
"clap",
"configparser",
"env_logger",
"log",
"network-interface",
"nmstate",
"serde",
"serde_yaml",
]
[[package]]
name = "nmstate"
version = "2.2.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f500fa292235468d9f3b1b98f5f220f7b4d21d51f1d78ae70794cfdfbc41b80"
dependencies = [
"log",
"nispor",
"nix 0.26.4",
"serde",
"serde_json",
"serde_yaml",
"tokio",
"uuid",
"zbus",
"zvariant",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rtnetlink"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb5850b5aa2c9c0ae44f157694bbe85107a2e13d76eb3178d0e3ee96c410f57"
dependencies = [
"futures",
"log",
"netlink-packet-core",
"netlink-packet-route",
"netlink-packet-utils",
"netlink-proto",
"netlink-sys",
"nix 0.29.0",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"indexmap",
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml_datetime"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
[[package]]
name = "toml_edit"
version = "0.22.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
]
[[package]]
name = "uds_windows"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
"memoffset",
"tempfile",
"winapi",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom",
"sha1_smol",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.1",
]
[[package]]
name = "wl-nl80211"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cffcf1e1dca38467779e22768bfc7f294f1b7b3bd99727edf13280eb2429789"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"byteorder",
"futures",
"genetlink",
"log",
"netlink-packet-core",
"netlink-packet-generic",
"netlink-packet-utils",
"netlink-proto",
"netlink-sys",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "zbus"
version = "5.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68"
dependencies = [
"async-broadcast",
"async-recursion",
"async-trait",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"nix 0.30.1",
"ordered-stream",
"serde",
"serde_repr",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.59.0",
"winnow",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
dependencies = [
"serde",
"static_assertions",
"winnow",
"zvariant",
]
[[package]]
name = "zvariant"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34"
dependencies = [
"proc-macro2",
"quote",
"serde",
"static_assertions",
"syn",
"winnow",
]
07070100000008000081A40000000000000000000000016835937A0000021B000000000000000000000000000000000000002100000000nm-configurator-0.3.3/Cargo.toml[package]
name = "nmc"
version = "0.3.3"
edition = "2021"
authors = ["Atanas Dinov <atanas.dinov@suse.com>"]
license = "Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.98"
clap = { version = "4.5.38", features = ["cargo"] }
env_logger = "0.11.6"
log = "0.4.27"
network-interface = "2.0.1"
nmstate = { version = "2.2.44", features = ["gen_conf"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_yaml = "0.9.34"
configparser = "3.1.0"
07070100000009000081A40000000000000000000000016835937A00000079000000000000000000000000000000000000002100000000nm-configurator-0.3.3/DockerfileFROM registry.suse.com/bci/rust:1.83
COPY . /
WORKDIR /
RUN cargo build --release --config net.git-fetch-with-cli=true
0707010000000A000081A40000000000000000000000016835937A00002C5D000000000000000000000000000000000000001E00000000nm-configurator-0.3.3/LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
0707010000000B000081A40000000000000000000000016835937A00001D85000000000000000000000000000000000000002000000000nm-configurator-0.3.3/README.md# nm-configurator
nm-configurator (or nmc) is a CLI tool which makes it easy to generate and apply NetworkManager configurations.
## How to install it?
### Download from release
Each release is published with nmc already built for `amd64` and `arm64` Linux systems:
```shell
$ curl -o nmc -L https://github.com/suse-edge/nm-configurator/releases/latest/download/nmc-linux-$(uname -m)
$ chmod +x nmc
```
### Build from source
```shell
$ git clone https://github.com/suse-edge/nm-configurator.git
$ cd nm-configurator
$ cargo build --release # only supports Linux based systems
```
## How to run it?
nmc depends on having the desired network state for all known nodes beforehand.
[NetworkManager](https://documentation.suse.com/sle-micro/5.5/html/SLE-Micro-all/cha-nm-configuration.html)
is using connection profiles defined as files stored under `/etc/NetworkManager/system-connections`.
In order to generate these config (*.nmconnection) files, nmc uses the
[nmstate](https://github.com/nmstate/nmstate) library and requires a configuration directory as an input.
This directory must contain the desired network state in one of the following file formats:
1. A single or multiple `<hostname>.yaml` files containing the different configurations per host.
This method requires specifying the MAC addresses of all Ethernet interfaces for each host
in order to properly identify the relevant configurations when applying those.
2. A single `_all.yaml` file containing the configurations to be applied to all hosts.
This method does not depend on MAC address matching.
nmc is then able to apply the generated configurations by identifying and storing the relevant NetworkManager settings for a given host.
Typically used with [Combustion](https://documentation.suse.com/sle-micro/5.5/single-html/SLE-Micro-deployment/#cha-images-combustion)
in order to bootstrap multiple nodes using the same provisioning artefact instead of depending on different custom images per machine.
### Per node configurations
#### Prepare desired states
```shell
mkdir -p desired-states
cat <<- EOF > desired-states/node1.yaml
interfaces:
- name: eth0
type: ethernet
state: up
mac-address: FE:C4:05:42:8B:AA
ipv4:
address:
- ip: 192.168.122.250
prefix-length: 24
enabled: true
ipv6:
address:
- ip: 2001:db8::1:1
prefix-length: 64
enabled: true
EOF
cat <<- EOF > desired-states/node2.yaml
interfaces:
- name: eth1
type: ethernet
state: up
mac-address: FE:C4:05:42:8B:AB
ipv4:
address:
- ip: 192.168.123.250
prefix-length: 24
enabled: true
ipv6:
enabled: false
EOF
cat <<- EOF > desired-states/node3.yaml
interfaces:
- name: eth4
type: ethernet
state: up
mac-address: FE:C4:05:42:8B:AC
ipv4:
address:
- ip: 192.168.124.250
prefix-length: 24
enabled: true
ipv6:
enabled: false
EOF
```
Please refer to the official nmstate docs for more extensive [examples](https://nmstate.io/examples.html).
#### Generate configurations
```shell
$ ./nmc generate --config-dir desired-states --output-dir network-config
[2024-11-12T09:05:57Z INFO nmc::generate_conf] Generating config from "desired-states/node1.yaml"...
[2024-11-12T09:05:59Z INFO nmc::generate_conf] Generating config from "desired-states/node2.yaml"...
[2024-11-12T09:05:59Z INFO nmc::generate_conf] Generating config from "desired-states/node3.yaml"...
[2024-11-12T09:05:59Z INFO nmc] Successfully generated and stored network config
$ find network-config | sort
network-config
network-config/host_config.yaml
network-config/node1
network-config/node1/eth0.nmconnection
network-config/node2
network-config/node2/eth1.nmconnection
network-config/node3
network-config/node3/eth4.nmconnection
```
There are separate directories for each host (identified by their input <i>hostname</i>.yaml).
Each of these contains the configuration files for the desired network interfaces (e.g. `eth0`).
The `host_config.yaml` file on the root level maps the hosts to all of their preconfigured interfaces.
This is necessary in order for nmc to identify which host it is running on when applying the network configurations later.
```yaml
- hostname: node1
interfaces:
- logical_name: eth0
connection_ids:
- eth0
mac_address: FE:C4:05:42:8B:AA
interface_type: ethernet
- hostname: node2
interfaces:
- logical_name: eth1
connection_ids:
- eth1
mac_address: FE:C4:05:42:8B:AB
interface_type: ethernet
- hostname: node3
interfaces:
- logical_name: eth4
connection_ids:
- eth4
mac_address: FE:C4:05:42:8B:AC
interface_type: ethernet
```
#### Apply configurations
Simply copy the directory containing the results from `nmc generate` (`network-config` in the example above) to the target host.
```shell
$ ./nmc apply --config-dir network-config
[2024-11-12T09:10:10Z INFO nmc::apply_conf] Identified host: node2
[2024-11-12T09:10:10Z INFO nmc::apply_conf] Set hostname: node2
[2024-11-12T09:10:11Z INFO nmc::apply_conf] Processing interface 'eth1'...
[2024-11-12T09:10:11Z INFO nmc::apply_conf] Processing connection 'eth1'...
[2024-11-12T09:10:11Z INFO nmc::apply_conf] Using interface name 'enp0s1' instead of the preconfigured 'eth1'
[2024-11-12T09:10:11Z INFO nmc] Successfully applied config
$ ls /etc/NetworkManager/system-connections
enp0s1.nmconnection
```
**NOTE:** Interface names during the installation of nodes might differ from the preconfigured logical ones.
This is expected and nmc will rely on the MAC addresses and use the actual names for the NetworkManager
configurations instead e.g. settings for interface with a predefined logical name `eth1` but actually named
`enp0s1` on the target node will automatically be adjusted and stored to `/etc/NetworkManager/enp0s1.nmconnection`.
### Unified configurations
There are occasions where relying on known MAC addresses is not an option.
In these cases we can opt for the so-called _unified_ configurations which allows us
to specify settings in an `_all.yaml` file which will then be applied across all provisioned nodes.
```shell
mkdir -p desired-states
cat <<- EOF > desired-states/_all.yaml
interfaces:
- name: eth0
type: ethernet
state: up
ipv4:
dhcp: true
enabled: true
ipv6:
enabled: false
- name: eth1
type: ethernet
state: up
ipv4:
address:
- ip: 10.0.0.1
prefix-length: 24
enabled: true
dhcp: false
ipv6:
enabled: false
EOF
```
#### Generate configurations:
```shell
$ ./nmc generate --config-dir desired-states --output-dir network-config
[2024-05-27T07:23:20Z INFO nmc::generate_conf] Generating config from "desired-states/_all.yaml"...
[2024-05-27T07:23:20Z INFO nmc] Successfully generated and stored network config
$ find network-config | sort
network-config
network-config/_all
network-config/_all/eth0.nmconnection
network-config/_all/eth1.nmconnection
```
**NOTE:** The `host_config.yaml` file will not be present since host mapping is not necessary.
#### Apply configurations:
```shell
$ ./nmc apply --config-dir network-config
[2024-05-27T07:24:03Z INFO nmc::apply_conf] Applying unified config...
[2024-05-27T07:24:03Z INFO nmc::apply_conf] Copying file... "network-config/_all/eth0.nmconnection"
[2024-05-27T07:24:03Z INFO nmc::apply_conf] Copying file... "network-config/_all/eth1.nmconnection"
[2024-05-27T07:24:03Z INFO nmc] Successfully applied config
$ ls /etc/NetworkManager/system-connections
eth0.nmconnection eth1.nmconnection
```
0707010000000C000041ED0000000000000000000000026835937A00000000000000000000000000000000000000000000001A00000000nm-configurator-0.3.3/src0707010000000D000081A40000000000000000000000016835937A0000645E000000000000000000000000000000000000002800000000nm-configurator-0.3.3/src/apply_conf.rsuse std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context};
use log::{debug, info, warn};
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
use nmstate::InterfaceType;
use crate::types::Host;
use crate::{ALL_HOSTS_DIR, HOST_MAPPING_FILE};
/// Destination directory to store the *.nmconnection files for NetworkManager.
const STATIC_SYSTEM_CONNECTIONS_DIR: &str = "/etc/NetworkManager/system-connections";
const RUNTIME_SYSTEM_CONNECTIONS_DIR: &str = "/var/run/NetworkManager/system-connections";
/// Configuration directory for NetworkManager options.
const CONFIG_DIR: &str = "/etc/NetworkManager/conf.d";
const CONNECTION_FILE_EXT: &str = "nmconnection";
const HOSTNAME_FILE: &str = "/etc/hostname";
pub(crate) fn apply(source_dir: &str) -> Result<(), anyhow::Error> {
let unified_config_path = Path::new(source_dir).join(ALL_HOSTS_DIR);
if unified_config_path.exists() {
info!("Applying unified config...");
copy_unified_connection_files(unified_config_path, STATIC_SYSTEM_CONNECTIONS_DIR)?;
} else {
let hosts = parse_hosts(source_dir).context("Parsing config")?;
debug!("Loaded hosts config: {hosts:?}");
let network_interfaces = NetworkInterface::show()?;
debug!("Retrieved network interfaces: {network_interfaces:?}");
let host = identify_host(hosts, &network_interfaces)
.ok_or_else(|| anyhow!("None of the preconfigured hosts match local NICs"))?;
info!("Identified host: {}", host.hostname);
fs::write(HOSTNAME_FILE, &host.hostname).context("Setting hostname")?;
info!("Set hostname: {}", host.hostname);
let local_interfaces = detect_local_interfaces(&host, network_interfaces);
copy_connection_files(
host,
local_interfaces,
source_dir,
STATIC_SYSTEM_CONNECTIONS_DIR,
)
.context("Copying connection files")?;
}
disable_wired_connections(CONFIG_DIR, RUNTIME_SYSTEM_CONNECTIONS_DIR)
.context("Disabling wired connections")
}
fn parse_hosts(source_dir: &str) -> Result<Vec<Host>, anyhow::Error> {
let config_file = Path::new(source_dir).join(HOST_MAPPING_FILE);
let file = fs::File::open(config_file)?;
let mut hosts: Vec<Host> = serde_yaml::from_reader(file)?;
// Ensure lower case formatting.
hosts.iter_mut().for_each(|h| {
h.interfaces.iter_mut().for_each(|i| match &i.mac_address {
None => {}
Some(addr) => i.mac_address = Some(addr.to_lowercase()),
});
});
Ok(hosts)
}
/// Identify the preconfigured static host by matching the MAC address of at least one of the local network interfaces.
fn identify_host(hosts: Vec<Host>, network_interfaces: &[NetworkInterface]) -> Option<Host> {
hosts.into_iter().find(|h| {
h.interfaces.iter().any(|interface| {
network_interfaces
.iter()
.filter(|nic| nic.mac_addr.is_some())
.any(|nic| nic.mac_addr == interface.mac_address)
})
})
}
/// Detect and return the differences between the preconfigured interfaces and their local representations.
///
/// Examples:
/// Desired Ethernet "eth0" -> Local "ens1f0"
/// Desired VLAN "eth0.1365" -> Local "ens1f0.1365"
fn detect_local_interfaces(
host: &Host,
network_interfaces: Vec<NetworkInterface>,
) -> HashMap<String, String> {
let mut local_interfaces = HashMap::new();
host.interfaces
.iter()
.filter(|interface| interface.interface_type == InterfaceType::Ethernet.to_string())
.for_each(|interface| {
let detected_interface = network_interfaces.iter().find(|nic| {
nic.mac_addr == interface.mac_address
&& !host.interfaces.iter().any(|i| i.logical_name == nic.name)
});
match detected_interface {
None => {}
Some(detected) => {
local_interfaces.insert(interface.logical_name.clone(), detected.name.clone());
}
};
});
// Look for non-Ethernet interfaces containing references to Ethernet ones differing from their preconfigured names.
local_interfaces.clone().iter().for_each(|(key, value)| {
host.interfaces
.iter()
.filter(|interface| {
interface.logical_name.contains(key) && !interface.logical_name.eq(key)
})
.for_each(|interface| {
let name = &interface.logical_name;
local_interfaces.insert(name.clone(), name.replace(key, value));
})
});
local_interfaces
}
/// Copy all *.nmconnection files from the preconfigured host dir to the
/// appropriate NetworkManager dir (default `/etc/NetworkManager/system-connections`).
fn copy_unified_connection_files(
source_dir: PathBuf,
destination_dir: &str,
) -> Result<(), anyhow::Error> {
fs::create_dir_all(destination_dir).context("Creating destination dir")?;
for entry in fs::read_dir(source_dir)? {
let entry = entry?;
let path = entry.path();
if entry.metadata()?.is_dir()
|| path
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.ne(CONNECTION_FILE_EXT)
{
warn!("Ignoring unexpected entry: {path:?}");
continue;
}
info!("Copying file... {path:?}");
let contents = fs::read_to_string(&path).context("Reading file")?;
let filename = path
.file_stem()
.and_then(OsStr::to_str)
.ok_or_else(|| anyhow!("Invalid file path"))?;
store_connection_file(filename, contents, destination_dir).context("Storing file")?;
}
Ok(())
}
/// Copy all *.nmconnection files from the preconfigured host dir to the
/// appropriate NetworkManager dir (default `/etc/NetworkManager/system-connections`)
/// applying interface naming adjustments if necessary.
fn copy_connection_files(
host: Host,
local_interfaces: HashMap<String, String>,
source_dir: &str,
destination_dir: &str,
) -> Result<(), anyhow::Error> {
fs::create_dir_all(destination_dir).context("Creating destination dir")?;
let host_config_dir = Path::new(source_dir).join(&host.hostname);
let host_config_dir = host_config_dir
.to_str()
.ok_or_else(|| anyhow!("Determining host config path"))?;
for interface in host.interfaces {
info!("Processing interface '{}'...", &interface.logical_name);
let connections = &interface.connection_ids;
if connections.is_empty() {
return Err(anyhow!(
"Missing connection ids for {}",
&interface.logical_name
));
}
for connection in connections {
info!("Processing connection '{}'...", connection);
let mut filename = connection.clone();
let filepath = keyfile_path(host_config_dir, &filename)
.ok_or_else(|| anyhow!("Determining source keyfile path"))?;
let mut contents = fs::read_to_string(filepath).context("Reading file")?;
// Update the name and all references of the host NIC in the settings file if there is a difference from the static config.
match local_interfaces.get(&interface.logical_name) {
None => {}
Some(local_name) => {
info!(
"Using interface name '{}' instead of the preconfigured '{}'",
local_name, interface.logical_name
);
contents = contents.replace(&interface.logical_name, local_name);
filename = filename.replace(&interface.logical_name, local_name);
}
}
store_connection_file(&filename, contents, destination_dir).context("Storing file")?;
}
}
Ok(())
}
fn store_connection_file(
filename: &str,
contents: String,
destination_dir: &str,
) -> Result<(), anyhow::Error> {
let destination = keyfile_path(destination_dir, filename)
.ok_or_else(|| anyhow!("Determining destination keyfile path"))?;
fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.mode(0o600)
.open(destination)
.context("Creating file")?
.write_all(contents.as_bytes())
.context("Writing file")
}
fn keyfile_path(dir: &str, filename: &str) -> Option<PathBuf> {
if dir.is_empty() || filename.is_empty() {
return None;
}
let mut destination = Path::new(dir).join(filename).into_os_string();
// Manually append the extension since Path::with_extension() would overwrite a portion of the
// filename (i.e. interface name) in the cases where the interface name contains one or more dots
destination.push(".");
destination.push(CONNECTION_FILE_EXT);
Some(destination.into())
}
fn disable_wired_connections(config_dir: &str, conn_dir: &str) -> Result<(), anyhow::Error> {
let _ = fs::remove_dir_all(conn_dir);
fs::create_dir_all(conn_dir).context(format!("Recreating {} directory", conn_dir))?;
fs::create_dir_all(config_dir).context(format!("Creating {} directory", config_dir))?;
let config_path = Path::new(config_dir).join("no-auto-default.conf");
let config_contents = "[main]\nno-auto-default=*\n";
fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(config_path)
.context("Creating config file")?
.write_all(config_contents.as_bytes())
.context("Writing config file")
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::{fs, io};
use network_interface::NetworkInterface;
use crate::apply_conf::{
copy_connection_files, copy_unified_connection_files, detect_local_interfaces,
disable_wired_connections, identify_host, keyfile_path, parse_hosts,
};
use crate::types::{Host, Interface};
#[test]
fn disable_wired_conn() {
assert!(disable_wired_connections("config", "connections").is_ok());
assert!(Path::new("config").exists());
assert!(Path::new("connections").exists());
let config_contents = fs::read_to_string("config/no-auto-default.conf").unwrap();
assert_eq!(config_contents, "[main]\nno-auto-default=*\n");
// cleanup
assert!(fs::remove_dir_all("config").is_ok());
assert!(fs::remove_dir_all("connections").is_ok());
}
#[test]
fn identify_host_successfully() {
let hosts = vec![
Host {
hostname: "h1".to_string(),
interfaces: vec![Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("00:11:22:33:44:55".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
}],
},
Host {
hostname: "h2".to_string(),
interfaces: vec![Interface {
logical_name: "".to_string(),
mac_address: Option::from("10:10:10:10:10:10".to_string()),
interface_type: "".to_string(),
connection_ids: Vec::new(),
}],
},
];
let interfaces = [
NetworkInterface {
name: "eth0".to_string(),
mac_addr: Some("00:11:22:33:44:55".to_string()),
addr: vec![],
index: 0,
},
NetworkInterface {
name: "eth0".to_string(),
mac_addr: Some("00:10:20:30:40:50".to_string()),
addr: vec![],
index: 0,
},
];
let host = identify_host(hosts, &interfaces).unwrap();
assert_eq!(host.hostname, "h1");
assert_eq!(
host.interfaces,
vec![Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("00:11:22:33:44:55".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
}]
);
}
#[test]
fn identify_host_fails() {
let hosts = vec![
Host {
hostname: "h1".to_string(),
interfaces: vec![Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("10:20:30:40:50:60".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
}],
},
Host {
hostname: "h2".to_string(),
interfaces: vec![Interface {
logical_name: "".to_string(),
mac_address: Option::from("00:10:20:30:40:50".to_string()),
interface_type: "".to_string(),
connection_ids: Vec::new(),
}],
},
];
let interfaces = [NetworkInterface {
name: "eth0".to_string(),
mac_addr: Some("00:11:22:33:44:55".to_string()),
addr: vec![],
index: 0,
}];
assert!(identify_host(hosts, &interfaces).is_none())
}
#[test]
fn parse_hosts_fails_due_to_missing_file() {
let error = parse_hosts("<missing>").unwrap_err();
assert!(error.to_string().contains("No such file or directory"))
}
#[test]
fn parse_hosts_successfully() {
let hosts = parse_hosts("testdata/apply/config").unwrap();
assert_eq!(
hosts,
vec![
Host {
hostname: "node1".to_string(),
interfaces: vec![
Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("00:11:22:33:44:55".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
},
Interface {
logical_name: "eth1".to_string(),
mac_address: Option::from("00:11:22:33:44:58".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth1".to_string()],
},
Interface {
logical_name: "eth2".to_string(),
mac_address: Option::from("36:5e:6b:a2:ed:80".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth2".to_string()],
},
Interface {
logical_name: "bond0".to_string(),
mac_address: Option::from("00:11:22:aa:44:58".to_string()),
interface_type: "bond".to_string(),
connection_ids: vec!["bond0".to_string()],
},
],
},
Host {
hostname: "node2".to_string(),
interfaces: vec![
Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("36:5e:6b:a2:ed:81".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
},
Interface {
logical_name: "eth0.1365".to_string(),
mac_address: None,
interface_type: "vlan".to_string(),
connection_ids: vec!["eth0.1365".to_string()],
},
],
},
Host {
hostname: "node3".to_string(),
interfaces: vec![
Interface {
logical_name: "br1".to_string(),
mac_address: None,
interface_type: "ovs-bridge".to_string(),
connection_ids: vec!["br1-br".to_string()],
},
Interface {
logical_name: "ovs0".to_string(),
mac_address: None,
interface_type: "ovs-interface".to_string(),
connection_ids: vec!["ovs0-port".to_string(), "ovs0-if".to_string()],
},
Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("95:b2:92:88:1d:3f".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string(), "eth0-port".to_string()],
},
],
},
]
)
}
#[test]
fn detect_interface_differences() {
let host = Host {
hostname: "node1".to_string(),
interfaces: vec![
Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("00:11:22:33:44:55".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
},
Interface {
logical_name: "eth0.1365".to_string(),
mac_address: None,
interface_type: "vlan".to_string(),
connection_ids: vec!["eth0.1365".to_string()],
},
Interface {
logical_name: "eth2".to_string(),
mac_address: Option::from("00:11:22:33:44:56".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth2".to_string()],
},
Interface {
logical_name: "eth2.bridge".to_string(),
mac_address: None,
interface_type: "linux-bridge".to_string(),
connection_ids: vec!["eth2.bridge".to_string()],
},
Interface {
logical_name: "bond0".to_string(),
mac_address: Option::from("00:11:22:33:44:58".to_string()),
interface_type: "bond".to_string(),
connection_ids: vec!["bond0".to_string()],
},
],
};
let interfaces = vec![
NetworkInterface {
name: "eth0".to_string(),
mac_addr: Some("00:11:22:33:44:55".to_string()),
addr: vec![],
index: 0,
},
NetworkInterface {
name: "eth0.1365".to_string(), // VLAN
addr: vec![],
mac_addr: Some("00:11:22:33:44:55".to_string()),
index: 0,
},
NetworkInterface {
name: "ens1f0".to_string(),
mac_addr: Some("00:11:22:33:44:56".to_string()),
addr: vec![],
index: 0,
},
];
let local_interfaces = detect_local_interfaces(&host, interfaces);
assert_eq!(
local_interfaces,
HashMap::from([
("eth2".to_string(), "ens1f0".to_string()),
("eth2.bridge".to_string(), "ens1f0.bridge".to_string())
])
)
}
#[test]
fn copy_unified_connection_files_successfully() -> io::Result<()> {
let source_dir = "testdata/apply/node1";
let destination_dir = "_all-out";
assert!(copy_unified_connection_files(source_dir.into(), destination_dir).is_ok());
let destination_path = Path::new(destination_dir);
for entry in fs::read_dir(source_dir)? {
let entry = entry?;
let filename = entry.file_name().into_string().unwrap();
let input = fs::read_to_string(entry.path())?;
let output = fs::read_to_string(destination_path.join(&filename))?;
assert_eq!(input, output);
}
// cleanup
fs::remove_dir_all(destination_dir)
}
#[test]
fn copy_connection_files_successfully() -> io::Result<()> {
let source_dir = "testdata/apply";
let destination_dir = "_out";
let host = Host {
hostname: "node1".to_string(),
interfaces: vec![
Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("00:11:22:33:44:55".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
},
Interface {
logical_name: "eth0.1365".to_string(),
mac_address: None,
interface_type: "vlan".to_string(),
connection_ids: vec!["eth0.1365".to_string()],
},
Interface {
logical_name: "br1".to_string(),
mac_address: None,
interface_type: "ovs-bridge".to_string(),
connection_ids: vec!["br1-br".to_string()],
},
Interface {
logical_name: "eth2".to_string(),
mac_address: Option::from("00:11:22:33:44:56".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth2".to_string(), "eth2-port".to_string()],
},
Interface {
logical_name: "eth1".to_string(),
mac_address: Option::from("00:11:22:33:44:57".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth1".to_string()],
},
Interface {
logical_name: "bond0".to_string(),
mac_address: Option::from("00:11:22:33:44:58".to_string()),
interface_type: "bond".to_string(),
connection_ids: vec!["bond0".to_string()],
},
],
};
let detected_interfaces = HashMap::from([("eth2".to_string(), "eth4".to_string())]);
assert!(copy_connection_files(
host,
detected_interfaces.clone(),
source_dir,
destination_dir
)
.is_ok());
let source_path = Path::new(source_dir).join("node1");
let destination_path = Path::new(destination_dir);
for entry in fs::read_dir(source_path)? {
let entry = entry?;
let mut filename = entry.file_name().into_string().unwrap();
let mut input = fs::read_to_string(entry.path())?;
// Adjust the name and content for the "eth2"->"eth4" edge case.
for (src_stem, dst_stem) in detected_interfaces.iter() {
if entry
.path()
.file_stem()
.is_some_and(|stem| stem.to_str().unwrap().contains(src_stem))
{
filename = filename.replace(src_stem, dst_stem);
input = input.replace(src_stem, dst_stem);
}
}
let output = fs::read_to_string(destination_path.join(&filename))?;
assert_eq!(input, output);
}
// cleanup
fs::remove_dir_all(destination_dir)
}
#[test]
fn copy_connection_files_missing_connection_ids() -> io::Result<()> {
let source_dir = "testdata/apply";
let destination_dir = "_out2";
let host = Host {
hostname: "node1".to_string(),
interfaces: vec![Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("00:11:22:33:44:55".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: Vec::new(),
}],
};
assert!(
copy_connection_files(host, HashMap::new(), source_dir, destination_dir)
.is_err_and(|e| e.to_string().contains("Missing connection ids"))
);
// cleanup
fs::remove_dir_all(destination_dir)
}
#[test]
fn generate_keyfile_path() {
assert_eq!(
keyfile_path("some-dir", "eth0"),
Some(PathBuf::from("some-dir/eth0.nmconnection"))
);
assert_eq!(
keyfile_path("some-dir", "eth0.1234"),
Some(PathBuf::from("some-dir/eth0.1234.nmconnection"))
);
assert!(keyfile_path("some-dir", "").is_none());
assert!(keyfile_path("", "eth0").is_none());
}
}
0707010000000E000081A40000000000000000000000016835937A00004D13000000000000000000000000000000000000002B00000000nm-configurator-0.3.3/src/generate_conf.rsuse std::ffi::OsStr;
use std::fs;
use std::path::Path;
use crate::types::{Host, Interface};
use crate::{ALL_HOSTS_DIR, ALL_HOSTS_FILE, HOST_MAPPING_FILE};
use anyhow::{anyhow, Context};
use configparser::ini::Ini;
use log::{info, warn};
use nmstate::{InterfaceType, NetworkState};
/// `NetworkConfig` contains the generated configurations in the
/// following format: `Vec<(config_file_name, config_content>)`
type NetworkConfig = Vec<(String, String)>;
/// Generate network configurations from all YAML files in the `config_dir`
/// and store the result *.nmconnection files and host mapping (if applicable) under `output_dir`.
pub(crate) fn generate(config_dir: &str, output_dir: &str) -> anyhow::Result<()> {
let files_count = fs::read_dir(config_dir)?.count();
if files_count == 0 {
return Err(anyhow!("Empty config directory"));
} else if files_count == 1 {
let path = Path::new(config_dir).join(ALL_HOSTS_FILE);
if let Ok(contents) = fs::read_to_string(&path) {
info!("Generating config from {path:?}...");
let (_, config) = generate_config(contents, false)?;
return store_network_config(output_dir, ALL_HOSTS_DIR, config)
.context("Storing network config");
};
};
for entry in fs::read_dir(config_dir)? {
let entry = entry?;
let path = entry.path();
if entry.metadata()?.is_dir() {
warn!("Ignoring unexpected dir: {path:?}");
continue;
}
info!("Generating config from {path:?}...");
let hostname = extract_hostname(&path)
.and_then(OsStr::to_str)
.ok_or_else(|| anyhow!("Invalid file path"))?
.to_owned();
let data = fs::read_to_string(&path).context("Reading network config")?;
let (interfaces, config) = generate_config(data, true)?;
store_network_config(output_dir, &hostname, config).context("Storing network config")?;
store_network_mapping(output_dir, hostname, interfaces)
.context("Storing network mapping")?;
}
Ok(())
}
fn extract_hostname(path: &Path) -> Option<&OsStr> {
if path
.extension()
.is_some_and(|ext| ext == "yml" || ext == "yaml")
{
path.file_stem()
} else {
path.file_name()
}
}
fn generate_config(
data: String,
require_mac_addresses: bool,
) -> anyhow::Result<(Vec<Interface>, NetworkConfig)> {
let network_state = NetworkState::new_from_yaml(&data)?;
let mut interfaces = extract_interfaces(&network_state);
validate_interfaces(&interfaces, require_mac_addresses)?;
let config = network_state
.gen_conf()?
.get("NetworkManager")
.ok_or_else(|| anyhow!("Invalid NM configuration"))?
.to_owned();
populate_connection_ids(&mut interfaces, &config)?;
validate_connection_ids(&interfaces)?;
Ok((interfaces, config))
}
fn validate_connection_ids(interfaces: &[Interface]) -> anyhow::Result<()> {
let empty_connection_ids: Vec<String> = interfaces
.iter()
.filter(|i| i.connection_ids.is_empty())
.map(|i| i.logical_name.to_owned())
.collect();
if !empty_connection_ids.is_empty() {
return Err(anyhow!(
"Detected interfaces without connection files: {}",
empty_connection_ids.join(", ")
));
};
Ok(())
}
fn populate_connection_ids(
interfaces: &mut [Interface],
config: &NetworkConfig,
) -> anyhow::Result<()> {
for (filename, content) in config {
let mut c = Ini::new();
c.read(content.to_string()).map_err(|e| anyhow!(e))?;
if c.get("connection", "type").is_some_and(|t| t == "loopback") {
continue;
}
let interface_name = c.get("connection", "interface-name");
let mac_address = c.get("ethernet", "mac-address");
if mac_address.is_none() && interface_name.is_none() {
return Err(anyhow!(
"No identifier found in connection file: {} (expected interface-name or mac-address)",
filename
));
}
let connection_id = c
.get("connection", "id")
.ok_or_else(|| anyhow!("No connection id found in connection file: {}", filename))?;
interfaces
.iter_mut()
.find(|x| {
if let Some(mac_address) = &mac_address {
if let Some(imac) = x.mac_address.as_ref() {
return imac.to_lowercase() == mac_address.to_lowercase();
}
}
if let Some(iname) = &interface_name {
return x.logical_name == *iname;
}
false
})
.ok_or_else(|| {
anyhow!(
"No matching interface found for connection file: {}",
filename
)
})?
.connection_ids
.push(connection_id);
}
Ok(())
}
fn extract_interfaces(network_state: &NetworkState) -> Vec<Interface> {
network_state
.interfaces
.iter()
.filter(|i| i.iface_type() != InterfaceType::Loopback)
.map(|i| Interface {
logical_name: i.name().to_owned(),
mac_address: i.base_iface().mac_address.clone(),
interface_type: i.iface_type().to_string(),
connection_ids: Vec::new(),
})
.collect()
}
fn validate_interfaces(
interfaces: &[Interface],
require_mac_addresses: bool,
) -> anyhow::Result<()> {
let ethernet_interfaces: Vec<&Interface> = interfaces
.iter()
.filter(|i| i.interface_type == InterfaceType::Ethernet.to_string())
.collect();
if ethernet_interfaces.is_empty() {
return Err(anyhow!("No Ethernet interfaces were provided"));
}
if !require_mac_addresses {
return Ok(());
}
let ethernet_interfaces: Vec<String> = ethernet_interfaces
.iter()
.filter(|i| i.mac_address.is_none())
.map(|i| i.logical_name.to_owned())
.collect();
if !ethernet_interfaces.is_empty() {
return Err(anyhow!(
"Detected Ethernet interfaces without a MAC address: {}",
ethernet_interfaces.join(", ")
));
};
Ok(())
}
fn store_network_config(
output_dir: &str,
hostname: &str,
config: NetworkConfig,
) -> anyhow::Result<()> {
let path = Path::new(output_dir).join(hostname);
fs::create_dir_all(&path).context("Creating output dir")?;
config.iter().try_for_each(|(filename, content)| {
let path = path.join(filename);
fs::write(path, content).context("Writing config file")
})
}
fn store_network_mapping(
output_dir: &str,
hostname: String,
interfaces: Vec<Interface>,
) -> anyhow::Result<()> {
let path = Path::new(output_dir);
let mapping_file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(path.join(HOST_MAPPING_FILE))?;
let hosts = [Host {
hostname,
interfaces,
}];
serde_yaml::to_writer(mapping_file, &hosts).context("Writing mapping file")
}
#[cfg(test)]
mod tests {
use crate::generate_conf::{
extract_hostname, extract_interfaces, generate, generate_config, populate_connection_ids,
validate_connection_ids, validate_interfaces,
};
use crate::types::{Host, Interface};
use crate::HOST_MAPPING_FILE;
use std::fs;
use std::path::Path;
#[test]
fn generate_successfully() -> Result<(), anyhow::Error> {
let config_dir = "testdata/generate";
let exp_output_path = Path::new("testdata/generate/expected");
let out_dir = "_out";
let output_path = Path::new("_out").join("node1");
generate(config_dir, out_dir)?;
// verify contents of lo.nmconnection files
let exp_lo_conn = fs::read_to_string(exp_output_path.join("lo.nmconnection"))?;
let lo_conn = fs::read_to_string(output_path.join("lo.nmconnection"))?;
assert_eq!(exp_lo_conn, lo_conn);
// verify contents of the host mapping file
let mut exp_hosts: Vec<Host> = serde_yaml::from_str(
fs::read_to_string(exp_output_path.join(HOST_MAPPING_FILE))?.as_str(),
)?;
let mut hosts: Vec<Host> = serde_yaml::from_str(
fs::read_to_string(Path::new(out_dir).join(HOST_MAPPING_FILE))?.as_str(),
)?;
assert_eq!(exp_hosts.len(), hosts.len());
exp_hosts.sort_by(|a, b| a.hostname.cmp(&b.hostname));
hosts.sort_by(|a, b| a.hostname.cmp(&b.hostname));
for (h1, h2) in exp_hosts.iter_mut().zip(hosts.iter_mut()) {
h1.interfaces
.sort_by(|a, b| a.logical_name.cmp(&b.logical_name));
h2.interfaces
.sort_by(|a, b| a.logical_name.cmp(&b.logical_name));
}
assert_eq!(exp_hosts, hosts);
// verify contents of *.nmconnection files based on interface.connection_ids
hosts
.iter_mut()
.flat_map(|h| h.interfaces.iter())
.flat_map(|interface| &interface.connection_ids)
.for_each(|conn_id| {
let exp_conn =
fs::read_to_string(exp_output_path.join(format!("{conn_id}.nmconnection")))
.unwrap();
let conn = fs::read_to_string(output_path.join(format!("{conn_id}.nmconnection")))
.unwrap();
assert_eq!(exp_conn, conn);
});
// cleanup
fs::remove_dir_all(out_dir)?;
Ok(())
}
#[test]
fn generate_fails_due_to_empty_dir() {
fs::create_dir_all("empty").unwrap();
let error = generate("empty", "_out").unwrap_err();
assert_eq!(error.to_string(), "Empty config directory");
fs::remove_dir_all("empty").unwrap();
}
#[test]
fn generate_fails_due_to_missing_path() {
let error = generate("<missing>", "_out").unwrap_err();
assert!(error.to_string().contains("No such file or directory"))
}
#[test]
fn generate_config_fails_due_to_invalid_data() {
let err = generate_config("<invalid>".to_string(), false).unwrap_err();
assert!(err.to_string().contains("Invalid YAML string"))
}
#[test]
fn extract_interfaces_skips_loopback() -> Result<(), serde_yaml::Error> {
let net_state: nmstate::NetworkState = serde_yaml::from_str(
r#"---
interfaces:
- name: eth1
type: ethernet
mac-address: FE:C4:05:42:8B:AA
- name: bridge0
type: linux-bridge
mac-address: FE:C4:05:42:8B:AB
- name: lo
type: loopback
mac-address: 00:00:00:00:00:00
"#,
)?;
let config_files = vec![
generate_config_file("eth1".to_string(), "eth1".to_string()),
generate_config_file("bridge0".to_string(), "bridge0".to_string()),
];
let mut interfaces = extract_interfaces(&net_state);
populate_connection_ids(&mut interfaces, &config_files).expect("populate ids");
interfaces.sort_by(|a, b| a.logical_name.cmp(&b.logical_name));
assert_eq!(
interfaces,
vec![
Interface {
logical_name: "bridge0".to_string(),
mac_address: Option::from("FE:C4:05:42:8B:AB".to_string()),
interface_type: "linux-bridge".to_string(),
connection_ids: vec!["bridge0".to_string()],
},
Interface {
logical_name: "eth1".to_string(),
mac_address: Option::from("FE:C4:05:42:8B:AA".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth1".to_string()],
},
]
);
Ok(())
}
fn generate_config_file(logical_name: String, connection_id: String) -> (String, String) {
let filename = format!("{connection_id}.nmconnection");
let mut config = configparser::ini::Ini::new();
config.set("connection", "id", Some(connection_id));
config.set("connection", "interface-name", Some(logical_name));
(filename, config.writes())
}
#[test]
fn validate_interfaces_missing_ethernet_interfaces() {
let interfaces = vec![
Interface {
logical_name: "eth3.1365".to_string(),
mac_address: None,
interface_type: "vlan".to_string(),
connection_ids: vec!["eth3.1365".to_string()],
},
Interface {
logical_name: "bond0".to_string(),
mac_address: None,
interface_type: "bond".to_string(),
connection_ids: vec!["bond0".to_string()],
},
];
let error = validate_interfaces(&interfaces, false).unwrap_err();
assert_eq!(error.to_string(), "No Ethernet interfaces were provided")
}
#[test]
fn validate_interfaces_missing_mac_addresses() {
let interfaces = vec![
Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("00:11:22:33:44:55".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
},
Interface {
logical_name: "eth1".to_string(),
mac_address: None,
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth1".to_string()],
},
Interface {
logical_name: "eth2".to_string(),
mac_address: Option::from("00:11:22:33:44:56".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth2".to_string()],
},
Interface {
logical_name: "eth3".to_string(),
mac_address: None,
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth3".to_string()],
},
Interface {
logical_name: "eth3.1365".to_string(),
mac_address: None,
interface_type: "vlan".to_string(),
connection_ids: vec!["eth3.1365".to_string()],
},
Interface {
logical_name: "bond0".to_string(),
mac_address: Option::from("00:11:22:33:44:58".to_string()),
interface_type: "bond".to_string(),
connection_ids: vec!["bond0".to_string()],
},
];
assert_eq!(
validate_interfaces(&interfaces, true)
.unwrap_err()
.to_string(),
"Detected Ethernet interfaces without a MAC address: eth1, eth3"
);
assert!(validate_interfaces(&interfaces, false).is_ok())
}
#[test]
fn validate_interfaces_missing_connection_ids() {
let interfaces = vec![
Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("00:11:22:33:44:55".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
},
Interface {
logical_name: "eth0.1365".to_string(),
mac_address: None,
interface_type: "vlan".to_string(),
connection_ids: vec!["eth0.1365".to_string()],
},
Interface {
logical_name: "bond0".to_string(),
mac_address: None,
interface_type: "bond".to_string(),
connection_ids: Vec::new(),
},
];
assert_eq!(
validate_connection_ids(&interfaces)
.unwrap_err()
.to_string(),
"Detected interfaces without connection files: bond0"
);
}
#[test]
fn validate_interfaces_successfully() {
let interfaces = vec![
Interface {
logical_name: "eth0".to_string(),
mac_address: Option::from("00:11:22:33:44:55".to_string()),
interface_type: "ethernet".to_string(),
connection_ids: vec!["eth0".to_string()],
},
Interface {
logical_name: "eth0.1365".to_string(),
mac_address: None,
interface_type: "vlan".to_string(),
connection_ids: vec!["eth0.1365".to_string()],
},
Interface {
logical_name: "bond0".to_string(),
mac_address: None,
interface_type: "bond".to_string(),
connection_ids: vec!["bond0".to_string()],
},
];
assert!(validate_interfaces(&interfaces, true).is_ok());
assert!(validate_interfaces(&interfaces, false).is_ok());
assert!(validate_connection_ids(&interfaces).is_ok());
}
#[test]
fn extract_host_name() {
assert_eq!(extract_hostname("".as_ref()), None);
assert_eq!(extract_hostname("node1".as_ref()), Some("node1".as_ref()));
assert_eq!(
extract_hostname("node1.example".as_ref()),
Some("node1.example".as_ref())
);
assert_eq!(
extract_hostname("node1.example.com".as_ref()),
Some("node1.example.com".as_ref())
);
assert_eq!(
extract_hostname("node1.example.com.yml".as_ref()),
Some("node1.example.com".as_ref())
);
assert_eq!(
extract_hostname("node1.example.com.yaml".as_ref()),
Some("node1.example.com".as_ref())
);
}
#[test]
fn test_populate_connection_ids() -> Result<(), anyhow::Error> {
let exp_output_path = Path::new("testdata/generate/expected");
let mut exp_hosts: Vec<Host> = serde_yaml::from_str(
fs::read_to_string(exp_output_path.join(HOST_MAPPING_FILE))?.as_str(),
)?;
let exp_ifaces = exp_hosts.pop().unwrap().interfaces;
let mut ifaces: Vec<Interface> = exp_ifaces.clone();
ifaces.iter_mut().for_each(|i| {
i.connection_ids = Vec::new();
});
let config = vec![
// By MAC Address case
(
"eth0.nmconnection".to_string(),
fs::read_to_string(exp_output_path.join("eth0.nmconnection"))?,
),
// By Name case
(
"eth1.nmconnection".to_string(),
fs::read_to_string(exp_output_path.join("eth1.nmconnection"))?,
),
];
populate_connection_ids(&mut ifaces, &config).unwrap();
let fake_config = r#"[connection]
autoconnect=true
autoconnect-slaves=1
id=ovs0-port
master=br1
slave-type=ovs-bridge
type=ovs-port
uuid=dde94eac-b114-55b9-8f5f-7d53334bcb78
[ovs-port]"#
.to_string();
let config = vec![("fake.nmconnection".to_string(), fake_config)];
assert_eq!(
populate_connection_ids(&mut ifaces, &config)
.unwrap_err()
.to_string(),
"No identifier found in connection file: fake.nmconnection (expected interface-name or mac-address)"
);
Ok(())
}
}
0707010000000F000081A40000000000000000000000016835937A00000FAB000000000000000000000000000000000000002200000000nm-configurator-0.3.3/src/main.rsuse log::{error, info};
use apply_conf::apply;
use generate_conf::generate;
mod apply_conf;
mod generate_conf;
mod types;
const APP_NAME: &str = "nmc";
const SUB_CMD_GENERATE: &str = "generate";
const SUB_CMD_APPLY: &str = "apply";
/// File storing a mapping between host identifier (usually hostname) and its preconfigured network interfaces.
const HOST_MAPPING_FILE: &str = "host_config.yaml";
/// File storing input configurations applicable for all hosts.
const ALL_HOSTS_FILE: &str = "_all.yaml";
/// Directory storing output configurations applicable for all hosts.
const ALL_HOSTS_DIR: &str = "_all";
fn main() {
let app = clap::Command::new(APP_NAME)
.version(clap::crate_version!())
.about("Command line of NM configurator")
.subcommand_required(true)
.subcommand(
clap::Command::new(SUB_CMD_GENERATE)
.about("Generate network configuration using nmstate")
.arg(
clap::Arg::new("CONFIG-DIR")
.required(true)
.long("config-dir")
.help("Config dir containing network configurations for different hosts in YAML format"),
)
.arg(
clap::Arg::new("OUTPUT-DIR")
.default_value("_out")
.long("output-dir")
.help("Destination dir storing the output configurations"),
))
.subcommand(
clap::Command::new(SUB_CMD_APPLY)
.about("Apply network configurations to host")
.arg(
clap::Arg::new("CONFIG-DIR")
.long("config-dir")
.default_value("config")
.help("Config dir containing host mapping ('host_config.yaml') \
and subdirectories containing *.nmconnection files per host")
)
.arg(
clap::Arg::new("VERBOSE")
.long("verbose")
.action(clap::ArgAction::SetTrue)
.help("Enables DEBUG log level")
)
);
let matches = app.get_matches();
match matches.subcommand() {
Some((SUB_CMD_GENERATE, cmd)) => {
let config_dir = cmd
.get_one::<String>("CONFIG-DIR")
.expect("--config-dir is required");
let output_dir = cmd
.get_one::<String>("OUTPUT-DIR")
.expect("--output-dir is required");
setup_logger(cmd);
match generate(config_dir, output_dir) {
Ok(..) => {
info!("Successfully generated and stored network config");
}
Err(err) => {
error!("Generating config failed: {err:#}");
std::process::exit(1)
}
}
}
Some((SUB_CMD_APPLY, cmd)) => {
let config_dir = cmd
.get_one::<String>("CONFIG-DIR")
.expect("--config-dir is required");
setup_logger(cmd);
match apply(config_dir) {
Ok(..) => {
info!("Successfully applied config");
}
Err(err) => {
error!("Applying config failed: {err:#}");
std::process::exit(1)
}
}
}
_ => unreachable!("Unrecognized subcommand"),
}
}
fn setup_logger(matches: &clap::ArgMatches) {
let verbose_arg = "VERBOSE";
let mut log_builder = env_logger::Builder::new();
if matches
.try_get_one::<bool>(verbose_arg)
.is_ok_and(|arg| arg.is_some_and(|&value| value))
{
log_builder.filter(None, log::LevelFilter::Debug);
} else {
log_builder.filter(None, log::LevelFilter::Info);
}
log_builder.init();
}
07070100000010000081A40000000000000000000000016835937A00000249000000000000000000000000000000000000002300000000nm-configurator-0.3.3/src/types.rsuse serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Host {
pub(crate) hostname: String,
pub(crate) interfaces: Vec<Interface>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Interface {
pub(crate) logical_name: String,
#[serde(default)]
pub(crate) connection_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub(crate) mac_address: Option<String>,
pub(crate) interface_type: String,
}
07070100000011000041ED0000000000000000000000026835937A00000000000000000000000000000000000000000000001F00000000nm-configurator-0.3.3/testdata07070100000012000041ED0000000000000000000000026835937A00000000000000000000000000000000000000000000002500000000nm-configurator-0.3.3/testdata/apply07070100000013000041ED0000000000000000000000026835937A00000000000000000000000000000000000000000000002C00000000nm-configurator-0.3.3/testdata/apply/config07070100000014000081A40000000000000000000000016835937A000004B9000000000000000000000000000000000000003D00000000nm-configurator-0.3.3/testdata/apply/config/host_config.yaml- hostname: node1
interfaces:
- logical_name: eth0
mac_address: 00:11:22:33:44:55
interface_type: ethernet
connection_ids:
- eth0
- logical_name: eth1
mac_address: 00:11:22:33:44:58
interface_type: ethernet
connection_ids:
- eth1
- logical_name: eth2
mac_address: 36:5e:6b:a2:ed:80
interface_type: ethernet
connection_ids:
- eth2
- logical_name: bond0
mac_address: 00:11:22:AA:44:58
interface_type: bond
connection_ids:
- bond0
- hostname: node2
interfaces:
- logical_name: eth0
mac_address: 36:5E:6B:A2:ED:81
interface_type: ethernet
connection_ids:
- eth0
- logical_name: eth0.1365
interface_type: vlan
connection_ids:
- eth0.1365
- hostname: node3
interfaces:
- logical_name: br1
connection_ids:
- br1-br
interface_type: ovs-bridge
- logical_name: ovs0
connection_ids:
- ovs0-port
- ovs0-if
interface_type: ovs-interface
- logical_name: eth0
connection_ids:
- eth0
- eth0-port
mac_address: 95:B2:92:88:1D:3F
interface_type: ethernet07070100000015000041ED0000000000000000000000026835937A00000000000000000000000000000000000000000000002B00000000nm-configurator-0.3.3/testdata/apply/node107070100000016000081A40000000000000000000000016835937A0000013D000000000000000000000000000000000000003E00000000nm-configurator-0.3.3/testdata/apply/node1/bond0.nmconnection[connection]
autoconnect = true
autoconnect-slaves = 1
id = bond0
interface-name = bond0
type = bond
uuid = 925b4a95-2de0-5b2d-bcf5-8b684a7e9cb4
[bond]
miimon = 140
mode = balance-rr
[ipv4]
address0 = 192.0.2.0/24
method = manual
[ipv6]
method = disabled
07070100000017000081A40000000000000000000000016835937A00000108000000000000000000000000000000000000003F00000000nm-configurator-0.3.3/testdata/apply/node1/br1-br.nmconnection[connection]
autoconnect=true
autoconnect-slaves=1
id=br1-br
interface-name=br1
type=ovs-bridge
uuid=327127e8-ee26-5178-bacd-ebbb191b1bdd
[ipv4]
dhcp-timeout=2147483647
method=disabled
[ipv6]
dhcp-timeout=2147483647
method=disabled
[ovs-bridge]
stp-enable=true
07070100000018000081A40000000000000000000000016835937A0000019C000000000000000000000000000000000000004200000000nm-configurator-0.3.3/testdata/apply/node1/eth0.1365.nmconnection[connection]
autoconnect=true
autoconnect-slaves=-1
id=eth0.1365
interface-name=eth0.1365
type=vlan
uuid=7544e8b2-96c6-594b-adcb-6f949ef3f9ba
[ipv4]
dhcp-client-id=mac
dhcp-send-hostname=true
dhcp-timeout=2147483647
ignore-auto-dns=false
ignore-auto-routes=false
method=auto
never-default=false
[ipv6]
dhcp-timeout=2147483647
method=disabled
[vlan]
flags=0
id=1365
parent=4fd00f34-9191-481c-b931-caa24dae871a
07070100000019000081A40000000000000000000000016835937A000001B7000000000000000000000000000000000000003D00000000nm-configurator-0.3.3/testdata/apply/node1/eth0.nmconnection[connection]
id = eth0
uuid = 4fd00f34-9191-481c-b931-caa24dae871a
type = ethernet
interface-name = eth0
[ethernet]
[ipv4]
address1 = 192.168.123.1/24
dns = 192.168.123.100
dns-priority = 40
method = manual
route1 = 0.0.0.0/0,192.168.123.1
route1_options = table=254
[ipv6]
addr-gen-mode = eui64
dhcp-duid = ll
dhcp-iaid = mac
method = disabled
[proxy]
0707010000001A000081A40000000000000000000000016835937A000001B7000000000000000000000000000000000000003D00000000nm-configurator-0.3.3/testdata/apply/node1/eth1.nmconnection[connection]
id = eth1
uuid = 4fd00f34-9191-481c-b931-caa24dae871a
type = ethernet
interface-name = eth1
[ethernet]
[ipv4]
address1 = 192.168.123.2/24
dns = 192.168.123.100
dns-priority = 40
method = manual
route1 = 0.0.0.0/0,192.168.123.2
route1_options = table=254
[ipv6]
addr-gen-mode = eui64
dhcp-duid = ll
dhcp-iaid = mac
method = disabled
[proxy]
0707010000001B000081A40000000000000000000000016835937A000000BA000000000000000000000000000000000000004200000000nm-configurator-0.3.3/testdata/apply/node1/eth2-port.nmconnection[connection]
autoconnect=true
autoconnect-slaves=-1
id=eth2-port
interface-name=eth2
master=br1
slave-type=ovs-bridge
type=ovs-port
uuid=5e4ac41f-d8dd-5cbd-98b3-122629af91e0
[ovs-port]
0707010000001C000081A40000000000000000000000016835937A000000BA000000000000000000000000000000000000003D00000000nm-configurator-0.3.3/testdata/apply/node1/eth2.nmconnection[connection]
autoconnect=true
autoconnect-slaves=-1
id=eth2
interface-name=eth2
master=eth2
slave-type=ovs-port
type=802-3-ethernet
uuid=0523c0a1-5f5e-5603-bcf2-68155d5d322e
[ethernet]
0707010000001D000041ED0000000000000000000000026835937A00000000000000000000000000000000000000000000002800000000nm-configurator-0.3.3/testdata/generate0707010000001E000041ED0000000000000000000000026835937A00000000000000000000000000000000000000000000003100000000nm-configurator-0.3.3/testdata/generate/expected0707010000001F000081A40000000000000000000000016835937A0000016F000000000000000000000000000000000000004500000000nm-configurator-0.3.3/testdata/generate/expected/br1-br.nmconnection[connection]
autoconnect=true
autoconnect-slaves=1
id=br1-br
interface-name=br1
type=ovs-bridge
uuid=327127e8-ee26-5178-bacd-ebbb191b1bdd
[ipv4]
dhcp-timeout=2147483647
method=disabled
[ipv6]
dhcp-timeout=2147483647
method=disabled
[ovs-bridge]
stp-enable=true
[user]
nmstate.interface.description=ovs bridge with eth1 as a port and ovs0 as an internal interface
07070100000020000081A40000000000000000000000016835937A0000014C000000000000000000000000000000000000004600000000nm-configurator-0.3.3/testdata/generate/expected/bridge0.nmconnection[connection]
autoconnect=true
autoconnect-slaves=1
id=bridge0
interface-name=bridge0
type=bridge
uuid=de801aaa-9c69-5f94-ae9b-705b10e92f9c
[bridge]
[ipv4]
address0=10.88.0.1/16
dhcp-timeout=2147483647
method=manual
[ipv6]
addr-gen-mode=0
dhcp-timeout=2147483647
method=link-local
[ethernet]
cloned-mac-address=FE:C4:05:42:8B:AA
07070100000021000081A40000000000000000000000016835937A00000142000000000000000000000000000000000000004300000000nm-configurator-0.3.3/testdata/generate/expected/eth0.nmconnection[connection]
autoconnect=true
autoconnect-slaves=-1
id=eth0
type=802-3-ethernet
uuid=dfd202f5-562f-5f07-8f2a-a7717756fb70
[ipv4]
address0=192.168.75.4/24
dhcp-timeout=2147483647
method=manual
[ipv6]
addr-gen-mode=0
dhcp-timeout=2147483647
method=link-local
[ethernet]
auto-negotiate=false
mac-address=0E:4D:C6:B8:C4:72
07070100000022000081A40000000000000000000000016835937A000000B9000000000000000000000000000000000000004800000000nm-configurator-0.3.3/testdata/generate/expected/eth1-port.nmconnection[connection]
autoconnect=true
autoconnect-slaves=1
id=eth1-port
interface-name=eth1
master=br1
slave-type=ovs-bridge
type=ovs-port
uuid=5e4ac41f-d8dd-5cbd-98b3-122629af91e0
[ovs-port]
07070100000023000081A40000000000000000000000016835937A000000DF000000000000000000000000000000000000004300000000nm-configurator-0.3.3/testdata/generate/expected/eth1.nmconnection[connection]
autoconnect=true
autoconnect-slaves=-1
id=eth1
interface-name=eth1
master=eth1
slave-type=ovs-port
type=802-3-ethernet
uuid=0523c0a1-5f5e-5603-bcf2-68155d5d322e
[ethernet]
cloned-mac-address=5C:C7:C9:5E:FB:EC
07070100000024000081A40000000000000000000000016835937A0000029D000000000000000000000000000000000000004200000000nm-configurator-0.3.3/testdata/generate/expected/host_config.yaml- hostname: node1
interfaces:
- logical_name: br1
connection_ids:
- br1-br
interface_type: ovs-bridge
- logical_name: bridge0
connection_ids:
- bridge0
mac_address: FE:C4:05:42:8B:AA
interface_type: linux-bridge
- logical_name: ovs0
connection_ids:
- ovs0-port
- ovs0-if
interface_type: ovs-interface
- logical_name: eth0
connection_ids:
- eth0
mac_address: 0E:4D:C6:B8:C4:72
interface_type: ethernet
- logical_name: eth1
connection_ids:
- eth1-port
- eth1
mac_address: 5c:c7:c9:5e:fb:ec
interface_type: ethernet
07070100000025000081A40000000000000000000000016835937A00000116000000000000000000000000000000000000004100000000nm-configurator-0.3.3/testdata/generate/expected/lo.nmconnection[connection]
autoconnect=true
autoconnect-slaves=-1
id=lo
interface-name=lo
type=loopback
uuid=e40b7973-b220-5450-bc07-1d87edc4aff2
[ipv4]
address0=127.0.0.1/8
dhcp-timeout=2147483647
method=manual
[ipv6]
addr-gen-mode=0
address0=::1/128
dhcp-timeout=2147483647
method=manual
07070100000026000081A40000000000000000000000016835937A00000199000000000000000000000000000000000000004600000000nm-configurator-0.3.3/testdata/generate/expected/ovs0-if.nmconnection[connection]
autoconnect=true
autoconnect-slaves=-1
id=ovs0-if
interface-name=ovs0
master=ovs0
slave-type=ovs-port
type=ovs-interface
uuid=94e89542-80b4-59a0-b84a-7d82c89c9ed4
[ipv4]
dhcp-client-id=mac
dhcp-send-hostname=true
dhcp-timeout=2147483647
ignore-auto-dns=false
ignore-auto-routes=false
method=auto
never-default=false
[ipv6]
dhcp-timeout=2147483647
method=disabled
[ovs-interface]
type=internal
07070100000027000081A40000000000000000000000016835937A000000B9000000000000000000000000000000000000004800000000nm-configurator-0.3.3/testdata/generate/expected/ovs0-port.nmconnection[connection]
autoconnect=true
autoconnect-slaves=1
id=ovs0-port
interface-name=ovs0
master=br1
slave-type=ovs-bridge
type=ovs-port
uuid=dde94eac-b114-55b9-8f5f-7d53334bcb78
[ovs-port]
07070100000028000081A40000000000000000000000016835937A0000073D000000000000000000000000000000000000003300000000nm-configurator-0.3.3/testdata/generate/node1.yamldns-resolver: {}
routes:
running:
- destination: 0.0.0.0/0
next-hop-interface: eth0
next-hop-address: 192.168.75.1
table-id: 254
config: []
interfaces:
- name: bridge0
type: linux-bridge
state: up
mac-address: FE:C4:05:42:8B:AA
ipv4:
enabled: true
address:
- ip: 10.88.0.1
prefix-length: 16
ipv6:
enabled: true
address:
- ip: fe80::fcc4:5ff:fe42:8baa
prefix-length: 64
- name: eth0
type: ethernet
state: up
identifier: mac-address
mac-address: 0E:4D:C6:B8:C4:72
ipv4:
enabled: true
address:
- ip: 192.168.75.4
prefix-length: 24
ipv6:
enabled: true
autoconf: false
address:
- ip: fdbb:5774:7b3e:da29:a589:1601:cb3:bc2e
prefix-length: 64
valid-left: 561235sec
preferred-left: 42676sec
- ip: fdbb:5774:7b3e:da29:c4d:c6ff:feb8:c472
prefix-length: 64
valid-left: 2591924sec
preferred-left: 604724sec
- ip: fe80::c4d:c6ff:feb8:c472
prefix-length: 64
ethernet:
auto-negotiation: false
- name: lo
type: loopback
state: up
mac-address: 00:00:00:00:00:00
mtu: 65536
ipv4:
enabled: true
address:
- ip: 127.0.0.1
prefix-length: 8
ipv6:
enabled: true
address:
- ip: ::1
prefix-length: 128
- name: eth1
type: ethernet
state: up
mac-address: 5c:c7:c9:5e:fb:ec
- name: ovs0
type: ovs-interface
state: up
ipv4:
dhcp: true
enabled: true
- name: br1
description: ovs bridge with eth1 as a port and ovs0 as an internal interface
type: ovs-bridge
state: up
bridge:
options:
stp: true
port:
- name: eth1
- name: ovs007070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!244 blocks