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
openSUSE Build Service is sponsored by