pax_global_header00006660000000000000000000000064145700414520014514gustar00rootroot0000000000000052 comment=45f7cd1569896d9e316c130bf5c60b7ccfc8211d netplan-1.0/000077500000000000000000000000001457004145200130155ustar00rootroot00000000000000netplan-1.0/.coveragerc000066400000000000000000000000441457004145200151340ustar00rootroot00000000000000[run] concurrency = multiprocessing netplan-1.0/.editorconfig000066400000000000000000000001641457004145200154730ustar00rootroot00000000000000root = true [*.{c,h,py}] indent_style = space indent_size = 4 [*.{yml,yaml}] indent_style = space indent_size = 2 netplan-1.0/.github/000077500000000000000000000000001457004145200143555ustar00rootroot00000000000000netplan-1.0/.github/pull_request_template.md000066400000000000000000000004411457004145200213150ustar00rootroot00000000000000 ## Description ## Checklist - [ ] Runs `make check` successfully. - [ ] Retains 100% code coverage (`make check-coverage`). - [ ] New/changed keys in YAML format are documented. - [ ] \(Optional\) Adds example YAML for new feature. - [ ] \(Optional\) Closes an open bug in Launchpad. netplan-1.0/.github/workflows/000077500000000000000000000000001457004145200164125ustar00rootroot00000000000000netplan-1.0/.github/workflows/automatic-doc-checks.yml000066400000000000000000000041711457004145200231270ustar00rootroot00000000000000name: Automatic documentation checks on: pull_request: paths: - 'doc/**' push: paths: - 'doc/**' workflow_dispatch: paths: - 'doc/**' workflow_call: inputs: working-directory: description: 'Working directory' required: true type: string default: 'doc' python-version: description: 'Version of the Python interpreter to use (defaults to 3.10)' required: false type: string default: "3.10" outputs: spellcheck-result: description: "Result of the spelling check" value: ${{ jobs.docchecks.outputs.result_spelling }} woke-result: description: "Result of the inclusive language check" value: ${{ jobs.docchecks.outputs.result_woke }} linkcheck-result: description: "Result of the link check" value: ${{ jobs.docchecks.outputs.result_links }} concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: docchecks: name: Run documentation checks runs-on: ubuntu-20.04 outputs: result_spelling: ${{ steps.spellcheck-step.outcome }} result_woke: ${{ steps.woke-step.outcome }} result_links: ${{ steps.linkcheck-step.outcome }} steps: - name: Add Doxygen uses: ssciwr/doxygen-install@v1 - name: Checkout uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ inputs.python-version }} - name: Spell Check id: spellcheck-step if: success() || failure() uses: canonical/documentation-workflows/spellcheck@main with: working-directory: 'doc' - name: Inclusive Language Check id: woke-step if: success() || failure() uses: canonical/documentation-workflows/inclusive-language@main with: working-directory: 'doc' - name: Link Check id: linkcheck-step if: success() || failure() uses: canonical/documentation-workflows/linkcheck@main with: working-directory: 'doc' netplan-1.0/.github/workflows/autopkgtest.yml000066400000000000000000000057411457004145200215160ustar00rootroot00000000000000name: Autopkgtest CI # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the main branch on: push: branches: [ main ] paths-ignore: - 'doc/**' pull_request: branches: [ '**' ] paths-ignore: - 'doc/**' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: lxd-ubuntu-jammy: # The type of runner that the job will run on runs-on: ubuntu-22.04 # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 # Setup LXD + Docker fixes - uses: canonical/setup-lxd@v0.1.1 with: channel: latest/stable # switch from distro's LTS channel to latest/stable - run: | git fetch --unshallow --tags # Install openvswitch-switch to make the OVS integration tests work # Install linux-modules-extra-azure to provide the 'vrf' kernel module, # it's needed (will be auto-loaded) by routing.test_vrf_basic - name: Install dependencies run: | sudo apt update sudo apt install autopkgtest ubuntu-dev-tools devscripts openvswitch-switch linux-modules-extra-$(uname -r) # work around LP: #1878225 as fallback - name: Preparing autopkgtest-build-lxd run: | sudo patch /usr/bin/autopkgtest-build-lxd .github/workflows/snapd.patch autopkgtest-build-lxd ubuntu-daily:jammy - name: Prepare test run: | pull-lp-source netplan.io cp -r netplan.io-*/debian . rm -r debian/patches/ # clear any distro patches # usrmerge-fix sed -i 's|rm debian/tmp/lib/netplan/generate|sh -c "mkdir -p debian/tmp/usr/lib/systemd/system-generators; rm -rf debian/tmp/lib; ln -sf /usr/libexec/netplan/generate debian/tmp/usr/lib/systemd/system-generators/netplan"|' debian/rules # usrmerge-fix-end sed -i 's| systemd-dev|# DELETED: systemd-dev|' debian/control TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) # find latest (stable) tag REV=$(git rev-parse --short HEAD) # get current git revision VER="$TAG+git~$REV" dch -v "$VER" "Autopkgtest CI testing (Jammy)" - name: Run autopkgtest (incl. build) run: | # using --setup-commands temporarily to install: # cmocka/pytest/rich/ethtool until they become proper test-deps # The netplan-ci PPA is used here temporally to fix some quirks: # - A bug with veths and network-manager 1.36. See LP: #2032824 # - A bug with gre6/vti6 and systemd-networkd 249.11. See LP: #2037667 autopkgtest . \ --setup-commands='sudo add-apt-repository -y -u -s ppa:slyon/netplan-ci' \ -U --env=DPKG_GENSYMBOLS_CHECK_LEVEL=0 --env=DEB_BUILD_OPTIONS=nocheck -- lxd autopkgtest/ubuntu/jammy/amd64 netplan-1.0/.github/workflows/build-abi.yml000066400000000000000000000032751457004145200207740ustar00rootroot00000000000000name: Build & ABI compatibility # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the main branch on: push: branches: [ main ] paths-ignore: - 'doc/**' pull_request: branches: [ main ] paths-ignore: - 'doc/**' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-22.04 # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 # Installs the build dependencies # Always include phased updates (LP: #1979244) - name: Install build depends run: | echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates sudo apt update sudo apt install abigail-tools ubuntu-dev-tools devscripts equivs pull-lp-source netplan.io sed -i 's| systemd-dev|# DELETED: systemd-dev|' netplan.io-*/debian/control mk-build-deps -i -B -s sudo netplan.io-*/debian/control # Runs the build - name: Run build run: | meson setup _build -Dunit_testing=false --prefix=/usr meson compile -C _build # Abigail ABI checker - name: Check ABI compatibility run: | abidiff abi-compat/jammy_1.0.xml _build/src/libnetplan.so.1 --headers-dir2 include/ --header-file2 src/abi.h --suppressions abi-compat/suppressions.abignore --no-added-syms netplan-1.0/.github/workflows/check-address-sanitizer.yml000066400000000000000000000020271457004145200236440ustar00rootroot00000000000000name: Check for memory issues # This action will compile netplan with ASAN (address sanitizer) and run # all the C unit tests and call the generator for every single file in the # examples directory. # The job will fail if a memory issue is detected by ASAN. on: push: branches: [ main ] paths-ignore: - 'doc/**' pull_request: branches: [ '**' ] paths-ignore: - 'doc/**' jobs: memory-sanitizer: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Install build depends run: | echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates sudo apt update sudo apt -y install curl expect ubuntu-dev-tools devscripts equivs pull-lp-source netplan.io sed -i 's| systemd-dev|# DELETED: systemd-dev|' netplan.io-*/debian/control mk-build-deps -i -B -s sudo netplan.io-*/debian/control - name: Run unit tests run: | unbuffer ./tools/run_asan.sh netplan-1.0/.github/workflows/check-coverage.yml000066400000000000000000000040701457004145200220040ustar00rootroot00000000000000name: Unit tests & Coverage # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the main branch on: push: branches: [ main ] pull_request: branches: [ '**' ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "test-and-coverage" test-and-coverage: # The type of runner that the job will run on runs-on: ubuntu-22.04 # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 # Installs the build dependencies, simulating a TTY/PTY via 'unbuffer' to # to fix test_terminal.py on GA (https://github.com/actions/runner/issues/241) # Always include phased updates (LP: #1979244) - name: Install build depends run: | echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates sudo apt update sudo apt install curl expect ubuntu-dev-tools devscripts equivs gcovr pull-lp-source netplan.io sed -i 's| systemd-dev|# DELETED: systemd-dev|' netplan.io-*/debian/control mk-build-deps -i -B -s sudo netplan.io-*/debian/control rm -rf netplan.io-*/ wget http://archive.ubuntu.com/ubuntu/pool/universe/g/gcovr/gcovr_5.2-1_all.deb sudo dpkg -i gcovr*.deb # we need newer gcovr to make the gcovr.cfg:exclude setting work # Runs the unit tests with coverage - name: Run unit tests run: | meson setup _build-cov --prefix=/usr -Db_coverage=true -Dunit_testing=true meson compile -C _build-cov unbuffer meson test -C _build-cov --verbose # Checks the coverage diff to the main branch #- name: Upload coverage to Codecov # uses: codecov/codecov-action@v1 # with: # name: check-coverage # fail_ci_if_error: true # verbose: true netplan-1.0/.github/workflows/check-wording.yaml000066400000000000000000000005301457004145200220200ustar00rootroot00000000000000name: Check for non-inclusive language on: push: branches: [ main ] pull_request: branches: [ '**' ] jobs: build: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Run Woke action uses: get-woke/woke-action@v0 with: fail-on-error: true woke-args: -o text netplan-1.0/.github/workflows/codeql-analysis.yml000066400000000000000000000053161457004145200222320ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main ] paths-ignore: - 'doc/**' pull_request: # The branches below must be a subset of the branches above branches: [ main ] paths-ignore: - 'doc/**' schedule: - cron: '17 21 * * 2' jobs: analyze: name: Analyze runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: language: [ 'cpp', 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Installs the build dependencies - name: Install build depends run: | sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list sudo apt update sudo apt install meson python3-coverage python3-pytest python3-pytest-cov libcmocka-dev python3-cffi libpython3-dev sudo apt build-dep netplan.io # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 netplan-1.0/.github/workflows/coverity.yml000066400000000000000000000033131457004145200210010ustar00rootroot00000000000000name: Coverity on: schedule: - cron: '0 0 * * MON' jobs: coverity: if: github.repository == 'canonical/netplan' runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Install dependencies run: | echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates sudo apt update sudo apt -y install curl ubuntu-dev-tools equivs pull-lp-source netplan.io sed -i 's| systemd-dev|# DELETED: systemd-dev|' netplan.io-*/debian/control mk-build-deps -i -B -s sudo netplan.io-*/debian/control - name: Download Coverity run: | curl https://scan.coverity.com/download/cxx/linux64 --no-progress-meter --output ${HOME}/coverity.tar.gz --data "token=${{ secrets.COVERITY_TOKEN }}&project=Netplan" mkdir ${HOME}/coverity tar --strip=1 -C ${HOME}/coverity -xzf ${HOME}/coverity.tar.gz echo "$HOME/coverity/bin" >> $GITHUB_PATH - name: Run Coverity run: | meson setup coveritybuild --prefix=/usr cov-build --dir cov-int meson compile -C coveritybuild tar czf netplan.tar.gz cov-int - name: Upload results run: | git fetch --unshallow --tags TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) # find latest (stable) tag REV=$(git rev-parse --short HEAD) # get current git revision VER="$TAG+git~$REV" curl --form token=${{ secrets.COVERITY_TOKEN }} --form email=${{ secrets.COVERITY_EMAIL }} --form file=@netplan.tar.gz --form version="${VER}" --form description="Coverity scan" https://scan.coverity.com/builds?project=Netplan netplan-1.0/.github/workflows/debci.yml000066400000000000000000000057351457004145200202150ustar00rootroot00000000000000name: Autopkgtest DebCI # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the main branch on: push: branches: [ main, 'stable/**' ] paths-ignore: - 'doc/**' pull_request: branches: [ '**' ] paths-ignore: - 'doc/**' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: lxc-debian-testing: # The type of runner that the job will run on runs-on: ubuntu-22.04 # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 - run: | git fetch --unshallow --tags # Install openvswitch-switch to make the OVS integration tests work # Install linux-modules-extra-azure to provide the 'vrf' kernel module, # it's needed (will be auto-loaded) by routing.test_vrf_basic - name: Install dependencies run: | sudo apt update sudo apt install debci lxc lxc-templates debian-archive-keyring autopkgtest ubuntu-dev-tools devscripts linux-modules-extra-$(uname -r) #openvswitch-switch # See: https://discourse.ubuntu.com/t/containers-lxc/11526 (Apparmor section) # (LP: #1950787, LP: #1998943) - name: Preparing autopkgtest-build-lxc run: | # Fix Docker blocking LXC networking: # https://discuss.linuxcontainers.org/t/9953/4 sudo iptables -I DOCKER-USER -j ACCEPT sudo apparmor_parser -R /etc/apparmor.d/usr.bin.lxc-start sudo ln -s /etc/apparmor.d/usr.bin.lxc-start /etc/apparmor.d/disable/ echo "lxc.apparmor.profile = unconfined" | sudo tee -a /etc/lxc/default.conf sudo debci setup -s testing -a amd64 -b lxc - name: Prepare test run: | # pull-debian-source netplan.io # snapshot.debian.org is not up-to-date V=$(rmadison -u debian -s unstable netplan.io | tail -n1 | cut -d"|" -f2 | xargs) dget -u "https://deb.debian.org/debian/pool/main/n/netplan.io/netplan.io_$V.dsc" cp -r netplan.io-*/debian . rm -r debian/patches/ # clear any distro patches sed -i 's|rm debian/tmp/lib/netplan/generate|# DELETED|' debian/rules TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) # find latest (stable) tag REV=$(git rev-parse --short HEAD) # get current git revision VER="$TAG+git~$REV" dch -v "$VER" "Autopkgtest CI testing (Debian testing)" - name: Run autopkgtest (incl. build) run: | # using --setup-commands='apt -y install ...' temporarily to install # (test-/build-) deps until they become part of the packaging sudo autopkgtest . \ -U --env=DPKG_GENSYMBOLS_CHECK_LEVEL=0 --env=DEB_BUILD_OPTIONS=nocheck -- lxc autopkgtest-testing-amd64 || test $? -eq 2 # allow OVS test to be skipped (exit code = 2) netplan-1.0/.github/workflows/network-manager.yml000066400000000000000000000064151457004145200222440ustar00rootroot00000000000000name: NetworkManager Autopkgtest # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the main branch on: push: branches: [ main, 'stable/**' ] paths-ignore: - 'doc/**' pull_request: branches: [ '**' ] paths-ignore: - 'doc/**' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: lxd-network-manager: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 # Setup LXD + Docker fixes - uses: canonical/setup-lxd@v0.1.1 with: channel: latest/stable # switch from distro's LTS channel to latest/stable - run: | git fetch --unshallow --tags # Install openvswitch-switch to make the OVS integration tests work # Install linux-modules-extra-azure to provide the 'vrf' kernel module, # it's needed (will be auto-loaded) by routing.test_vrf_basic - name: Install dependencies run: | sudo apt update sudo apt install autopkgtest ubuntu-dev-tools devscripts openvswitch-switch linux-modules-extra-$(uname -r) - name: Prepare test run: | pull-lp-source netplan.io cp -r netplan.io-*/debian . rm -r debian/patches/ # clear any distro patches # usrmerge-fix sed -i 's|rm debian/tmp/lib/netplan/generate|sh -c "mkdir -p debian/tmp/usr/lib/systemd/system-generators; rm -rf debian/tmp/lib; ln -sf /usr/libexec/netplan/generate debian/tmp/usr/lib/systemd/system-generators/netplan"|' debian/rules # usrmerge-fix-end echo "3.0 (native)" > debian/source/format # force native build TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) # find latest (stable) tag REV=$(git rev-parse --short HEAD) # get current git revision VER="$TAG+git~$REV" dch -v "$VER" "Autopkgtest CI" # Build deb - uses: jtdor/build-deb-action@v1 env: DEB_BUILD_OPTIONS: nocheck DPKG_GENSYMBOLS_CHECK_LEVEL: 0 with: docker-image: ubuntu:noble buildpackage-opts: --build=binary --no-sign #extra-build-deps: python3-cffi libpython3-dev # work around LP: #1878225 as fallback - name: Preparing autopkgtest-build-lxd run: | sudo patch /usr/bin/autopkgtest-build-lxd .github/workflows/snapd.patch MIRROR=http://archive.ubuntu.com/ubuntu autopkgtest-build-lxd ubuntu-daily:noble # LP: #2052639 - name: Run autopkgtest run: | # using --setup-commands temporarily to install: # cmocka/pytest/rich/ethtool until they become proper test-deps pull-lp-source network-manager noble sudo autopkgtest -U \ --copy=debian/artifacts:/root/ --setup-commands='dpkg -i /root/*.deb' \ --env=DEB_BUILD_OPTIONS=nocheck \ --apt-pocket=proposed=src:network-manager \ network-manager_*.dsc -- lxd autopkgtest/ubuntu/noble/amd64 || test $? -eq 2 # allow for skipped tests (exit code = 2) netplan-1.0/.github/workflows/rpmbuild.yml000066400000000000000000000017231457004145200207560ustar00rootroot00000000000000name: RPM build on: push: branches: [ main, 'stable/**' ] paths-ignore: - 'doc/**' pull_request: branches: [ '**' ] paths-ignore: - 'doc/**' jobs: rpm: runs-on: ubuntu-latest strategy: fail-fast: false matrix: container: - fedora:latest # - fedora:rawhide - rockylinux:9 container: image: ${{ matrix.container }} steps: - uses: actions/checkout@v2 - name: Build & Test run: | cat /etc/os-release dnf -y install dnf-plugins-core rpmdevtools # for 'dnf builddep' dnf -y install epel-release || true dnf config-manager --set-enabled crb || true # Meson/CMocka on EL9 dnf -y install centos-release-nfv-openvswitch || true # OVS on EL9 dnf -y builddep rpm/netplan.spec adduser test chown -R test:test . su test -c 'rpmbuild -bi --build-in-place rpm/netplan.spec' netplan-1.0/.github/workflows/snapd.patch000066400000000000000000000004471457004145200205450ustar00rootroot00000000000000@@ -70,6 +70,8 @@ sleep 5 if lxc exec "$CONTAINER" -- systemctl mask serial-getty@getty.service; then + lxc exec "$CONTAINER" -- systemctl mask snapd.service + lxc exec "$CONTAINER" -- systemctl mask snapd.seeded.service lxc exec "$CONTAINER" -- reboot fi netplan-1.0/.github/workflows/spread.yml000066400000000000000000000007611457004145200204170ustar00rootroot00000000000000name: Run spread on: push: branches: [ main ] paths-ignore: - 'doc/**' pull_request: branches: [ main ] paths-ignore: - 'doc/**' jobs: spread: runs-on: ubuntu-latest steps: - uses: canonical/setup-lxd@v0.1.1 - uses: actions/checkout@v3 - name: Install spread run: | go install github.com/snapcore/spread/cmd/spread@latest - name: Run the spread test inside LXD run: | ~/go/bin/spread -v lxd: netplan-1.0/.gitignore000066400000000000000000000002501457004145200150020ustar00rootroot00000000000000generate netplan-dbus test-coverage doc/*.html doc/*.[1-9] __pycache__ *.pyc .coverage .vscode src/_features.h netplan_cli/_features.py dbus/io.netplan.Netplan.service netplan-1.0/.readthedocs.yaml000066400000000000000000000007571457004145200162550ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and environment build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the doc/ directory with Sphinx sphinx: configuration: doc/conf.py builder: dirhtml # Declare the Python requirements required to build your docs python: install: - requirements: doc/.sphinx/requirements.txt netplan-1.0/.woke.yaml000066400000000000000000000060261457004145200147300ustar00rootroot00000000000000ignore_files: - abi-compat/*.xml rules: # Including some terms from https://inclusivenaming.org/ that are not in the default ruleset - name: abort terms: - abort alternatives: - force quit - halt - close - name: cripple terms: - cripple alternatives: - degraded - restrict - name: segregate terms: - segregate - segregation alternatives: - segment/segmentation - separate/separation # From https://github.com/canonical/Inclusive-naming # Overtly racist terms - name: whitelist terms: - whitelist - white-list - whitelisted - white-listed - whitelisting - white-listing alternatives: - allowlist severity: warning - name: blacklist terms: - blacklist - black-list - blacklisted - black-listed - blacklisting - black-listing alternatives: - denylist - blocklist severity: warning - name: blackhat terms: - blackhat - black hat alternatives: - malicious actor - attacker severity: warning - name: illegal characters terms: - illegal characters alternatives: - invalid characters - unsupported characters severity: warning - name: master terms: - master alternatives: - primary - main severity: warning - name: slave terms: - slave alternatives: - secondary - replica severity: warning - name: whitehat terms: - whitehat - white hat alternatives: - researcher - security specialist severity: warning # Overtly Sexist/Transphobic/Homophobic/Default Male Gendered Terms/Pejorative about Gender Identity/Discriminative - name: chairman terms: - chairman - foreman alternatives: - chair - foreperson severity: warning - name: grandfathered terms: - grandfathered alternatives: - legacied severity: warning - name: guys terms: - guys alternatives: - people - folks severity: warning - name: hang terms: - hang alternatives: - stop responding - stall severity: warning options: word_boundary: true - name: man hours terms: - man hours alternatives: - staff hours - hours of effort severity: warning - name: man in the middle terms: - man in the middle - man-in-the-middle alternatives: - machine-in-the-middle - person-in-the-middle severity: warning - name: manned terms: - manned alternatives: - staffed - monitored severity: warning - name: middleman terms: - middleman alternatives: - middleperson - intermediary severity: warning - name: sanity check terms: - sanity alternatives: - confidence check - coherence check severity: warning # Ignore rules - name: he - name: dummy netplan-1.0/.wokeignore000077700000000000000000000000001457004145200201012doc/.wokeignoreustar00rootroot00000000000000netplan-1.0/CONTRIBUTING000066400000000000000000000047501457004145200146550ustar00rootroot00000000000000# Contributing to netplan.io Thanks for taking the time to contribute to netplan! Here are the guidelines for contributing to the development of netplan. These are guidelines, not hard and fast rules; but please exercise judgement. Feel free to propose changes to this document. #### Table Of Contents [Code of Conduct](#code-of-conduct) [What should I know before I get started](#what-should-i-know-before-i-get-started) * [Did you find a bug?](#did-you-find-a-bug) * [Code Quality](#code-quality) * [Conventions](#conventions) ## Code of Conduct This project and everyone participating in it is governed by the [Ubuntu Code of Conduct](https://www.ubuntu.com/community/code-of-conduct). By participating, you are expected to uphold this code. Please report unacceptable behavior to [netplan-developers@lists.launchpad.net](mailto:netplan-developers@lists.launchpad.net). ## What should I know before I get started? ### Did you find a bug? If you've found a bug, please make sure to report it on Launchpad against the [netplan](http://bugs.launchpad.net/netplan) project. We do use these bug reports to make it easier to backport features to supported releases of Ubuntu: having an existing bug report, with people who understand well how the bug appears helps immensely in making sure the feature or bug fix is well tested when it is being released to supported Ubuntu releases. ### Code quality We want to maintain the quality of the code in netplan to the highest possible degree. As such, we do insist on keeping a code coverage with unit tests to 100% coverage if it is possible. If not, please make sure to explain why when submitting a pull request, and expect reviewers to challenge you on that decision and suggest a course of action. ### Conventions The netplan project mixes C and python code. Generator code is generally all written in C, while the UI / command-line interface is written in python. Please look at the surrounding code, and make a best effort to follow the general style used in the code. We do insist on proper indentation (4 spaces), but we will not block good features and bug fixes on purely style issues. Please exercise your best judgement: if it looks odd or too clever to you, chances are it will look odd or too clever to code reviewers. In that case, you may be asked for some styles changes in a pull request. Similarly, if you see code that you find hard to understand, we do encourage that you submit pull requests that help make the code easier to understand and maintain. netplan-1.0/COPYING000066400000000000000000001045131457004145200140540ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . netplan-1.0/Makefile000066400000000000000000000026351457004145200144630ustar00rootroot00000000000000.PHONY: clean default check linting pre-coverage VER = $(shell meson introspect _build/ --projectinfo | jq -r '.version' && rm -rf _build) DESTDIR ?= ../tmproot default: _build meson compile -C _build --verbose _build: meson setup _build --prefix=/usr _build-cov: meson setup _build-cov --prefix=/usr -Db_coverage=true clean: rm -f netplan_cli/_features.py src/_features.h src/_features.h.gch rm -f generate doc/*.html doc/*.[1-9] rm -f *.o *.so* rm -f netplan-dbus dbus/*.service rm -f *.gcda *.gcno generate.info rm -f tests/ctests/*.gcda tests/ctests/*.gcno rm -rf test-coverage .coverage rm -f .coverage.* coverage.xml find . | grep -E "(__pycache__|\.pyc)" | xargs rm -rf rm -rf build rm -rf _build rm -rf _build-cov rm -rf _leakcheckbuild rm -rf _cleanbuild rm -rf tmproot rm -f python-cffi/netplan/_netplan_cffi.* rm -f tools/keyfile_to_yaml dist: clean _build tar --exclude="_build" --exclude=".git" --exclude="debian" --exclude=".vscode" -cvJf ../netplan-$(VER).tar.xz . ln -sf netplan-$(VER).tar.xz ../netplan.io_$(VER).orig.tar.xz # Debian .orig symlink check: default meson test -C _build --verbose linting: _build meson test -C _build --verbose linting meson test -C _build --verbose codestyle pre-coverage: _build-cov meson compile -C _build-cov --verbose check-coverage: pre-coverage meson test -C _build-cov --verbose install: default meson install -C _build --destdir $(DESTDIR) netplan-1.0/README.md000066400000000000000000000030501457004145200142720ustar00rootroot00000000000000# netplan - Backend-agnostic network configuration in YAML [![Build+ABI](https://github.com/canonical/netplan/workflows/Build%20&%20ABI%20compatibility/badge.svg?branch=main)](https://github.com/canonical/netplan/actions/workflows/build-abi.yml?query=branch%3Amain) [![Test+Coverage](https://github.com/canonical/netplan/workflows/Unit%20tests%20&%20Coverage/badge.svg?branch=main)](https://github.com/canonical/netplan/actions/workflows/check-coverage.yml?query=branch%3Amain) [![CI](https://github.com/canonical/netplan/workflows/Autopkgtest%20CI/badge.svg?branch=main)](https://github.com/canonical/netplan/actions/workflows/autopkgtest.yml?query=branch%3Amain) # Website http://netplan.io # Documentation An overview of the architecture can be found at [netplan.io/design](https://netplan.io/design) The full documentation for netplan is available in the [doc/](../main/doc/) directory. # Build using Meson Steps to build netplan using the [Meson](https://mesonbuild.com) build system inside the `build/` directory: * meson setup build --prefix=/usr [-Db_coverage=true] * meson compile -C build * meson test -C build --verbose [TEST_NAME] * meson install -C build --destdir ../tmproot # Bug reports Please file bug reports in [Launchpad](https://bugs.launchpad.net/netplan/+filebug). # Contact us Please join us on IRC in #netplan at [Libera.Chat](https://libera.chat/). Our mailing list is [here](https://lists.launchpad.net/netplan-developers/). Email the list at [netplan-developers@lists.launchpad.net](mailto:netplan-developers@lists.launchpad.net). netplan-1.0/TODO000066400000000000000000000021501457004145200135030ustar00rootroot00000000000000- improve IPv6 RA handling - support ethtool/sysctl knobs (TSO, LRO, txqueuelen) - inspecting current network config via "netplan show $interface" for a collated view of each interface's yaml. - debugging config generation via "netplan diff [backend|system]": - netplan diff system: compare generated config with current ip addr output - netplan diff backend: compare generated config with current config for backend - better handle VLAN Q-in-Q (mostly generation tweaks + patching backends) - support device aliases (eth0 + eth0.1; add eth0 to multiple bridges) - workaround for two bridges is to use eth0 and vlan1 - make errors translatable - "netplan save" to capture kernel state into netplan YAML. - better parsing/validation for time-based values (ie. bond, bridge params) - better parsing/validation for all schema - integrate 'netplan try' in tmux/screen - add automated integration tests for WPA Enterprise / 802.1x that can run self-contained # After soname bump (ABI break) - change route->scope to ENUM - move tunnel_ttl into tunnel struct - store match.driver as a list rather than a string netplan-1.0/abi-compat/000077500000000000000000000000001457004145200150315ustar00rootroot00000000000000netplan-1.0/abi-compat/README.md000066400000000000000000000013051457004145200163070ustar00rootroot00000000000000# Netplan's ABI checker We're using "abigail" (abigail-tools) to validate libnetplan's ABI. ## HowTo create a ABI reference The `abidw` tool can be used to generate an ABI XML like this: ``` meson setup _build --prefix=/usr meson compile -C _build abidw _build/src/libnetplan.so.1 --headers-dir include/ --header-file src/abi.h > abi-compat/jammy_1.0.xml ``` ## HowTo compare a ABI The `abidiff` tool can be used to compare a new library ABI to an existing XML reference like this (also, see .github/workflows/build-abi.yml): ``` abidiff abi-compat/jammy_1.0.xml _build/src/libnetplan.so.1 --headers-dir2 include/ --header-file2 src/abi.h --suppressions abi-compat/suppressions.abignore --no-added-syms ``` netplan-1.0/abi-compat/jammy_1.0.xml000066400000000000000000012641341457004145200172610ustar00rootroot00000000000000 netplan-1.0/abi-compat/suppressions.abignore000066400000000000000000000004101457004145200213110ustar00rootroot00000000000000# Ignore this type/file/function/variable during ABI checks # passed to abidiff using the --suppressions parameter # # Documentation about the syntax can be found here: # https://sourceware.org/libabigail/manual/libabigail-concepts.html#suppression-specifications netplan-1.0/dbus/000077500000000000000000000000001457004145200137525ustar00rootroot00000000000000netplan-1.0/dbus/io.netplan.Netplan.conf000066400000000000000000000011371457004145200202720ustar00rootroot00000000000000 netplan-1.0/dbus/io.netplan.Netplan.service.in000066400000000000000000000001751457004145200214130ustar00rootroot00000000000000[D-BUS Service] Name=io.netplan.Netplan Exec=@ROOTLIBEXECDIR@/netplan/netplan-dbus User=root AssumedAppArmorLabel=unconfined netplan-1.0/dbus/meson.build000066400000000000000000000017351457004145200161220ustar00rootroot00000000000000features_h = custom_target( build_always_stale: true, output: '_features.h', input: join_paths(meson.project_source_root(), 'features_h_generator.sh'), command: ['sh', '-c', '@INPUT@'], install: false, capture: true, ) executable( 'netplan-dbus', '../src/dbus.c', features_h, include_directories: inc, link_with: libnetplan, dependencies: [libsystemd, glib, gio, yaml, uuid], install_dir: join_paths(get_option('libexecdir'), 'netplan'), install: true) install_data( 'io.netplan.Netplan.conf', install_dir: join_paths(get_option('datadir'), 'dbus-1', 'system.d')) conf_data = configuration_data() conf_data.set('ROOTLIBEXECDIR', join_paths(get_option('prefix'), get_option('libexecdir'))) configure_file( input: 'io.netplan.Netplan.service.in', output: 'io.netplan.Netplan.service', configuration: conf_data, install: true, install_dir: join_paths(get_option('datadir'), 'dbus-1', 'system-services')) netplan-1.0/doc/000077500000000000000000000000001457004145200135625ustar00rootroot00000000000000netplan-1.0/doc/.custom_wordlist.txt000066400000000000000000000015001457004145200176360ustar00rootroot00000000000000 APN ARP CDMA CIDR CONFIG ConnectX Coverity D-Bus DF DHCP DNS DUID EAP Ethernet FDB Fi GRO GSM GSO IAID IANA IEC IGMP IOV IOV IoT IPoIB IPv InfiniBand LACPDUs LAI Lexicographically Libera LLADDR LLDP LRO LXD MAAS MCC MII MNC MTU Mantic Mellanox NTP Netplan NetworkManager OpenFlow OpenVPN PCI PSK RDNSS SIGINT SIGUSR SLAAC SR SSID SSIDs SSL STP SmartNIC TCP TLS TSO TTL TTLS UDP UUID VF VFs VLAN VLANs VNI VPN VRF VRFs VXLAN WPA WWAN WakeOnWLan Wi WireGuard adapters boolean checksumming checksums config curtin decrypt dir failover hostname ifname ifnames initramfs instantiation ip iptables json libnetplan libvirt libvirtd loopback miimon multicast netlink networkd nm programmatically renderer reselection runtime stateful stateful statelessly subnet systemd udev unencrypted untagged vSwitch Wi-Fi WireGuard wpasupplicant yaml netplan-1.0/doc/.gitignore000066400000000000000000000001441457004145200155510ustar00rootroot00000000000000_build .sphinx/venv/ .sphinx/breathe/ .sphinx/.doctrees/ .sphinx/warnings.txt .sphinx/.wordlist.dic netplan-1.0/doc/.sphinx/000077500000000000000000000000001457004145200151515ustar00rootroot00000000000000netplan-1.0/doc/.sphinx/_static/000077500000000000000000000000001457004145200165775ustar00rootroot00000000000000netplan-1.0/doc/.sphinx/_static/custom.css000066400000000000000000000114331457004145200206250ustar00rootroot00000000000000/** Fix the font weight (300 for normal, 400 for slightly bold) **/ div.page, h1, h2, h3, h4, h5, h6, .sidebar-tree .current-page>.reference, button, input, optgroup, select, textarea, th.head { font-weight: 300 } .toc-tree li.scroll-current>.reference, dl.glossary dt, dl.simple dt, dl:not([class]) dt { font-weight: 400; } /** Table styling **/ th.head { text-transform: uppercase; font-size: var(--font-size--small); } table.docutils { border: 0; box-shadow: none; width:100%; } table.docutils td, table.docutils th, table.docutils td:last-child, table.docutils th:last-child, table.docutils td:first-child, table.docutils th:first-child { border-right: none; border-left: none; } /* Allow to centre text horizontally in table data cells */ table.align-center { text-align: center !important; } /** No rounded corners **/ .admonition, code.literal, .sphinx-tabs-tab, .sphinx-tabs-panel, .highlight { border-radius: 0; } /** Admonition styling **/ .admonition { border-top: 1px solid #d9d9d9; border-right: 1px solid #d9d9d9; border-bottom: 1px solid #d9d9d9; } /** Color for the "copy link" symbol next to headings **/ a.headerlink { color: var(--color-brand-primary); } /** Line to the left of the current navigation entry **/ .sidebar-tree li.current-page { border-left: 2px solid var(--color-brand-primary); } /** Some tweaks for issue #16 **/ [role="tablist"] { border-bottom: 1px solid var(--color-sidebar-item-background--hover); } .sphinx-tabs-tab[aria-selected="true"] { border: 0; border-bottom: 2px solid var(--color-brand-primary); background-color: var(--color-sidebar-item-background--current); font-weight:300; } .sphinx-tabs-tab{ color: var(--color-brand-primary); font-weight:300; } .sphinx-tabs-panel { border: 0; border-bottom: 1px solid var(--color-sidebar-item-background--hover); background: var(--color-background-primary); } button.sphinx-tabs-tab:hover { background-color: var(--color-sidebar-item-background--hover); } /** Custom classes to fix scrolling in tables by decreasing the font size or breaking certain columns. Specify the classes in the Markdown file with, for example: ```{rst-class} break-col-4 min-width-4-8 ``` **/ table.dec-font-size { font-size: smaller; } table.break-col-1 td.text-left:first-child { word-break: break-word; } table.break-col-4 td.text-left:nth-child(4) { word-break: break-word; } table.min-width-1-15 td.text-left:first-child { min-width: 15em; } table.min-width-4-8 td.text-left:nth-child(4) { min-width: 8em; } /** Underline for abbreviations **/ abbr[title] { text-decoration: underline solid #cdcdcd; } /** Use the same style for right-details as for left-details **/ .bottom-of-page .right-details { font-size: var(--font-size--small); display: block; } /** Version switcher */ button.version_select { color: var(--color-foreground-primary); background-color: var(--color-toc-background); padding: 5px 10px; border: none; } .version_select:hover, .version_select:focus { background-color: var(--color-sidebar-item-background--hover); } .version_dropdown { position: relative; display: inline-block; text-align: right; font-size: var(--sidebar-item-font-size); } .available_versions { display: none; position: absolute; right: 0px; background-color: var(--color-toc-background); box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); z-index: 11; } .available_versions a { color: var(--color-foreground-primary); padding: 12px 16px; text-decoration: none; display: block; } .available_versions a:hover {background-color: var(--color-sidebar-item-background--current)} .show {display:block;} /** Fix for nested numbered list - the nested list is lettered **/ ol.arabic ol.arabic { list-style: lower-alpha; } /** Make expandable sections look like links **/ details summary { color: var(--color-link); } /** Fix the styling of the version box for readthedocs **/ #furo-readthedocs-versions .rst-versions, #furo-readthedocs-versions .rst-current-version, #furo-readthedocs-versions:focus-within .rst-current-version, #furo-readthedocs-versions:hover .rst-current-version { background: var(--color-sidebar-item-background--hover); } .rst-versions .rst-other-versions dd a { color: var(--color-link); } #furo-readthedocs-versions:focus-within .rst-current-version .fa-book, #furo-readthedocs-versions:hover .rst-current-version .fa-book, .rst-versions .rst-other-versions { color: var(--color-sidebar-link-text); } .rst-versions .rst-current-version { color: var(--color-version-popup); font-weight: bolder; } /* Code-block copybutton invisible by default (overriding Furo config to achieve default copybutton setting). */ .highlight button.copybtn { opacity: 0; } netplan-1.0/doc/.sphinx/_static/favicon.png000066400000000000000000000043221457004145200207330ustar00rootroot00000000000000PNG  IHDR szzgAMA a cHRMz&u0`:pQ<bKGDtIME  )(O`IDATXÕo7s/'sb'LMRCDK^zW(T zӅRj.R+lv',3b&U'7ϻe~ۧy70/$/i4,a@e ''{*xxӁX8h#N=K X^ &7ʓ<=ŪX7tJ)`?h,<#q P5YM[ȼE⌲n@ǀ}@ 9p>  lɨ0~;(`GutT*q#u`X>,aFAwS[q0pxS9,/(XejwNN|0V9W0.AV?"18'&YD}16`늭NtcVfl )˩s^h/z|Hbx/ڲLlqbzC0c? M9{-bwH$PU%Sq-Z?'Z(m!Aʚ& "ߛ:YN!FBRҹ-^F%%`Xt5Z+/$u6Kt oSX-@ YŘIRT#i7n20Nl_5lb.@rۆ>a(}HmpW!C}&%dܕiuMeKTN`MEin,ù> d8b,[&,&͌]_^%uRaL/nvh<3"-?S21Dl3Y9af/B =i;4|7챙`{nko`\m` x0J# r}_e32gآDVƌ MV {[lTNm/<`d "-p5ƾ,[AaX@LbdnIikaqc&|t_n"SOC)KfWtŹEE!"Ji jB&<*i5(q(JL;rͨXF}ZPFEy61~9"eTQpc="b212h(tǐwJ&5zYwHƹXfi%ݳ~_Q; djঠk2?dH@+瀏0g M>O\" 1FkEٰ= %)` =[$ݭ`yV{tk~l R QI{ FUP@]0 i/R=`~k(ib ($`@ ye wWjro|mU{CuIw 4 s7/]˪ǁmY.'r:Ͳh^:V`u{rGfQbM|.LϟiS?ӭm0(鿒Ka-R+ 2.rT:ZtmA SbO^R['F}4oϗ$mVhb\ C<֕4B.sk|ژ 4-Ю") y0=ΕZ( |5+A'`'.`:zSbdCX `|xJFrG8?mՍn% 6YY)_:eYwOMA`Yi_/Z@v/ (R8BMJc }>BVBeעD)6}D :İ9+1%tEXtdate:create2023-11-27T12:41:16+00:00a;%tEXtdate:modify2023-11-27T12:41:16+00:00هIENDB`netplan-1.0/doc/.sphinx/_static/furo_colors.css000066400000000000000000000107101457004145200216440ustar00rootroot00000000000000body { --color-code-background: #f8f8f8; --color-code-foreground: black; --font-stack: Ubuntu, -apple-system, Segoe UI, Roboto, Oxygen, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; --font-stack--monospace: Ubuntu Mono, Consolas, Monaco, Courier, monospace; --color-foreground-primary: #111; --color-foreground-secondary: var(--color-foreground-primary); --color-foreground-muted: #333; --color-background-secondary: #FFF; --color-background-hover: #f2f2f2; --color-brand-primary: #111; --color-brand-content: #06C; --color-api-background: #cdcdcd; --color-inline-code-background: rgba(0,0,0,.03); --color-sidebar-link-text: #111; --color-sidebar-item-background--current: #ebebeb; --color-sidebar-item-background--hover: #f2f2f2; --toc-font-size: var(--font-size--small); --color-admonition-title-background--note: var(--color-background-primary); --color-admonition-title-background--tip: var(--color-background-primary); --color-admonition-title-background--important: var(--color-background-primary); --color-admonition-title-background--caution: var(--color-background-primary); --color-admonition-title--note: #24598F; --color-admonition-title--tip: #24598F; --color-admonition-title--important: #C7162B; --color-admonition-title--caution: #F99B11; --color-highlighted-background: #EbEbEb; --color-link-underline: var(--color-background-primary); --color-link-underline--hover: var(--color-background-primary); --color-version-popup: #772953; } @media not print { body[data-theme="dark"] { --color-code-background: #202020; --color-code-foreground: #d0d0d0; --color-foreground-secondary: var(--color-foreground-primary); --color-foreground-muted: #CDCDCD; --color-background-secondary: var(--color-background-primary); --color-background-hover: #666; --color-brand-primary: #fff; --color-brand-content: #06C; --color-sidebar-link-text: #f7f7f7; --color-sidebar-item-background--current: #666; --color-sidebar-item-background--hover: #333; --color-admonition-background: transparent; --color-admonition-title-background--note: var(--color-background-primary); --color-admonition-title-background--tip: var(--color-background-primary); --color-admonition-title-background--important: var(--color-background-primary); --color-admonition-title-background--caution: var(--color-background-primary); --color-admonition-title--note: #24598F; --color-admonition-title--tip: #24598F; --color-admonition-title--important: #C7162B; --color-admonition-title--caution: #F99B11; --color-highlighted-background: #666; --color-link-underline: var(--color-background-primary); --color-link-underline--hover: var(--color-background-primary); --color-version-popup: #F29879; } @media (prefers-color-scheme: dark) { body:not([data-theme="light"]) { --color-code-background: #202020; --color-code-foreground: #d0d0d0; --color-foreground-secondary: var(--color-foreground-primary); --color-foreground-muted: #CDCDCD; --color-background-secondary: var(--color-background-primary); --color-background-hover: #666; --color-brand-primary: #fff; --color-brand-content: #06C; --color-sidebar-link-text: #f7f7f7; --color-sidebar-item-background--current: #666; --color-sidebar-item-background--hover: #333; --color-admonition-background: transparent; --color-admonition-title-background--note: var(--color-background-primary); --color-admonition-title-background--tip: var(--color-background-primary); --color-admonition-title-background--important: var(--color-background-primary); --color-admonition-title-background--caution: var(--color-background-primary); --color-admonition-title--note: #24598F; --color-admonition-title--tip: #24598F; --color-admonition-title--important: #C7162B; --color-admonition-title--caution: #F99B11; --color-highlighted-background: #666; --color-link-underline: var(--color-background-primary); --color-link-underline--hover: var(--color-background-primary); --color-version-popup: #F29879; } } } netplan-1.0/doc/.sphinx/_static/github_issue_links.css000066400000000000000000000007641457004145200232120ustar00rootroot00000000000000.github-issue-link-container { padding-right: 0.5rem; } .github-issue-link { font-size: var(--font-size--small); font-weight: bold; background-color: #DD4814; padding: 13px 23px; text-decoration: none; } .github-issue-link:link { color: #FFFFFF; } .github-issue-link:visited { color: #FFFFFF } .muted-link.github-issue-link:hover { color: #FFFFFF; text-decoration: underline; } .github-issue-link:active { color: #FFFFFF; text-decoration: underline; } netplan-1.0/doc/.sphinx/_static/github_issue_links.js000066400000000000000000000013201457004145200230230ustar00rootroot00000000000000// if we already have an onload function, save that one var prev_handler = window.onload; window.onload = function() { // call the previous onload function if (prev_handler) { prev_handler(); } const link = document.createElement("a"); link.classList.add("muted-link"); link.classList.add("github-issue-link"); link.text = "Give feedback"; link.href = ("https://bugs.launchpad.net/netplan/+filebug"); link.target = "_blank"; const div = document.createElement("div"); div.classList.add("github-issue-link-container"); div.append(link) const container = document.querySelector(".article-container > .content-icon-container"); container.prepend(div); }; netplan-1.0/doc/.sphinx/_static/header-nav.js000066400000000000000000000004051457004145200211460ustar00rootroot00000000000000$(document).ready(function() { $(document).on("click", function () { $(".more-links-dropdown").hide(); }); $('.nav-more-links').click(function(event) { $('.more-links-dropdown').toggle(); event.stopPropagation(); }); }) netplan-1.0/doc/.sphinx/_static/header.css000066400000000000000000000067131457004145200205500ustar00rootroot00000000000000.p-navigation { border-bottom: 1px solid var(--color-sidebar-background-border); } .p-navigation__nav { background: #333333; display: flex; } .p-logo { display: flex !important; padding-top: 0 !important; text-decoration: none; } .p-logo-image { height: 44px; padding-right: 10px; } .p-logo-text { margin-top: 18px; color: white; text-decoration: none; } ul.p-navigation__links { display: flex; list-style: none; margin-left: 0; margin-top: auto; margin-bottom: auto; max-width: 800px; width: 100%; } ul.p-navigation__links li { margin: 0 auto; text-align: center; width: 100%; } ul.p-navigation__links li a { background-color: rgba(0, 0, 0, 0); border: none; border-radius: 0; color: var(--color-sidebar-link-text); display: block; font-weight: 400; line-height: 1.5rem; margin: 0; overflow: hidden; padding: 1rem 0; position: relative; text-align: left; text-overflow: ellipsis; transition-duration: .1s; transition-property: background-color, color, opacity; transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); white-space: nowrap; width: 100%; } ul.p-navigation__links .p-navigation__link { color: #ffffff; font-weight: 300; text-align: center; text-decoration: none; } ul.p-navigation__links .p-navigation__link:hover { background-color: #2b2b2b; } ul.p-navigation__links .p-dropdown__link:hover { background-color: var(--color-sidebar-item-background--hover); } ul.p-navigation__links .p-navigation__sub-link { background: var(--color-background-primary); padding: .5rem 0 .5rem .5rem; font-weight: 300; } ul.p-navigation__links .more-links-dropdown li a { border-left: 1px solid var(--color-sidebar-background-border); border-right: 1px solid var(--color-sidebar-background-border); } ul.p-navigation__links .more-links-dropdown li:first-child a { border-top: 1px solid var(--color-sidebar-background-border); } ul.p-navigation__links .more-links-dropdown li:last-child a { border-bottom: 1px solid var(--color-sidebar-background-border); } ul.p-navigation__links .p-navigation__logo { padding: 0.5rem; } ul.p-navigation__links .p-navigation__logo img { width: 40px; } ul.more-links-dropdown { display: none; overflow-x: visible; height: 0; z-index: 55; padding: 0; position: relative; list-style: none; margin-bottom: 0; margin-top: 0; } .nav-more-links::after { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23111' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E"); background-position: center; background-repeat: no-repeat; background-size: contain; content: ""; display: block; filter: invert(100%); height: 1rem; pointer-events: none; position: absolute; right: 1rem; text-indent: calc(100% + 10rem); top: calc(1rem + 0.25rem); width: 1rem; } .nav-ubuntu-com { display: none; } @media only screen and (min-width: 480px) { ul.p-navigation__links li { width: 100%; } .nav-ubuntu-com { display: inherit; } } @media only screen and (max-width: 800px) { .nav-more-links { margin-left: auto !important; padding-right: 2rem !important; width: 8rem !important; } } @media only screen and (min-width: 800px) { ul.p-navigation__links li { width: 100% !important; } } @media only screen and (min-width: 1310px) { ul.p-navigation__links { margin-left: calc(50% - 41em); } } netplan-1.0/doc/.sphinx/_static/netplan.svg000066400000000000000000000061041457004145200207620ustar00rootroot00000000000000 image/svg+xml netplan-1.0/doc/.sphinx/_static/tag.png000066400000000000000000000151751457004145200200710ustar00rootroot00000000000000PNG  IHDRaݶ pHYs  ~/IDATx?P[GdƉҠQfg K`\ngPdg̝  v(3gׅVd ;bp$IB#W}ɽs86|ϟ54ιDO$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$'m 7ónoHLgH$j~o VWڸq(Ys{#H<CxwӟS+9v<n d "ݑΜ -`:c\oױY^t={/CG2@2_$~ܧJ7b}5x thi;Qͯ8?K1~Y5s ')g_aH_74|ᨃ[u7/PDmkokp,ѼRZ_$ORͯotŃq_s~zё(9fok + qx7>Aq5Lg7PoO-ZSo񅣈wG0~M t^BgPۏz|awuwrUS庆oNr `wIgƒcZƲ贖|(.~oξ!D'8?N5nLgUEǻï|[Eb)5B=Lg? 3pSSb{b1whlj[b \V*o^غEb)"unt  D'pyQf\xp̵p{\Hpy"ede}C=nz 0~D'pyvɘku[z[{_OM:|7ݣWᑟC]jkݧΚp`i|bspk#c'~_ɭagy<c;OQZziou-Z¯(-=ͺ " ߧKVGڞڗWW_d:N`6Sg,?E=my\NC?WξBq1*5ϼ`w#V[j"c·xe;xrzI}ɁֽwWoh|-:ڛ|ĩzbuÃc̬[ͯccjأQ9v߼0z*q)\uۿؘ04[s{3fwu"|g /׌fS/Aߍd SZ03F쭮` 7ÖZJ^#&~7V-Oyi grSqk ӞHLnN{JEBD@SY~j'EfF|G'v_+ygD w{3NUɭ_GKD[W51]agۛqJn 3#]NgatmOI?Ma~R@ߟ|ᨱ˒{*s̳H9l=/>|Ka~\֕)j%`:s`Ͽ< 57mxOSHg걿S'moLDBӷY}.>;w?kx~;sGPɭacjM[fv>N]ͯ8?Ҡk#]`:sdR?̓O9- :>tufAjtur64}\'N=+B9:h5EG> $S"Ҫog0uWWcs )t}bk?'=әH tˏUu|kIS;_n#cGy!)|eKw0?Ob'1?O"E'pi;@2>k YHmY/?@s5w#oԬƞ!ӏ`:tI-3U+Re#d?g72*~GO{+a/xX'JW[S(j.|{/~0+ӯdTx z{o`'p'dӍz:pyǻqi?L@fSH1?6>kFqy1?T:"ueoʩ[*8y]߬@ .>C r{S*uM96VVF'x7eHu G도h[e+ Q#ZٍyfPT>c©v;ЎJ0upUxQ Cն NyoS;pJԅ%9NM&HН?}pM  qPy<)L]Qc?˪9w6@: T~@"~%5uK|9z~FO⅝D[JE%wx>9Ss?O(\'{FtƣZo ?ׂuTͻx:4K==LiοƯ_Lx73fuD:Pu`66'ڧrRO1~}-r5OӪ8"r{4ˋ;4&OWnox'SbڎΡn^!Rw_?W.u\ix#Jng"~륷 $WrYjҥH=Us Z&4Hdf&e릩Tͯ:gUE`kq#I7{oj5Ņ{noFJRttCE,?u{3TɭtCT'_֣R3#@wD`sfD@T%*[Pkv:6h}*GZ5Ji6ְ15=HՑ_'kK15S}RL?wHi6&p5w3_`ˠۏd}_WvToygt e{l멽E%O*\V]j߲1z;+a Q5e[VW_$PO? : }}T ʫ+}ܓTÿ+v~]2#u-w6N%Em?IkC>[O/߄|M~"'z-YɭqVI˚qI߄> 'QTsN`: FQߤS~[''p [{:Sct|v~X# $SJyy*O՟DZ.'>Q0:_RjiqOEg \G0#Æ 34؀~3 S>_7y;ˋv{SF{{"1t$SΠʫ+ph)+߱Y>_7v_.ϿR}sԳA~zhϮ}lǡdp/^*=^OpoȱӬE'Z>@JE|3SSK3noʻCJgf@_V*O ::]7_G翻ی UK t?׻_=yww3_w#m;gŅ{M|f4N_5_n}5ύ[kvx%-x]wͯ>P_lRsԶ (O6Q\P@2sϔ?Ds 7nJ.G_f@wk扨@g..hhA]Sy>۶W yEoNhѠwzf|@៚k@}·WpC_AV*董> $jVxy'pک#u9_w ߚzOX"?Jnmϫ;jqξ\souߑrU4}[Y|gnF''fxTlc:QH7xBN󅣈޴7q7QG#DO5o񅣸8/F lw݀?5Y~{?K~'Wk8YoUޯ<5kH]^u`Qb_}xIO%2~>u`#H̨I53ԯwC=TN|B=ꖲnP?k|(bDQ{[Nm͙wU\9ۣ*Cj,N?w컁;; WeygDN퇮Սv: \ǥу)xwk44s J.tufݸpk/.~ξ!ogܼN猎Nky깎Pu}?̋dɔg&h?"*lq\ wkGpרQ^]A%E%/9}`ǻ+5Jf%g ZH^AG*"e~C*,6gf/>K0ն ؘp]'x=5TnoF[=vl$.cM(-=Ԡ4#՟f#fNSZzYx[]槴:u;]m`:uRmW"~V;𠽩KKv;x?o#v~[ [[[`.vkcpmJŶlϱygSY~A6O9hTq'Jtǡd퍠:_$PO?Bt1_$vҳl-uk_Sx~t !JE|3ۃbdJvt1A09\G@2j3%60~)t33 |[oH ]3O40?emO i d?xv:6'=G*5'9g2<' 'P?k8w&_́d itݍ}1~r^g"vbonk?Lg7ξggױuI=_$v0@ّL5tYP+ W!"G1~r8Qk-q.rE}rP^ bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$' bDB1~"?PH(O$'Xp }IENDB`netplan-1.0/doc/.sphinx/_templates/000077500000000000000000000000001457004145200173065ustar00rootroot00000000000000netplan-1.0/doc/.sphinx/_templates/base.html000066400000000000000000000004771457004145200211160ustar00rootroot00000000000000{% extends "furo/base.html" %} {% block theme_scripts %} {% endblock theme_scripts %} {# ru-fu: don't include the color variables from the conf.py file, but use a  separate CSS file to save space #} {% block theme_styles %} {% endblock theme_styles %} netplan-1.0/doc/.sphinx/_templates/footer.html000066400000000000000000000064241457004145200215000ustar00rootroot00000000000000{# ru-fu: copied from Furo, with modifications as stated below. Modifications are marked 'mod:'. #}
{%- if show_copyright %} {%- endif %} {# mod: removed "Made with" #} {%- if last_updated -%}
{% trans last_updated=last_updated|e -%} Last updated on {{ last_updated }} {%- endtrans -%}
{%- endif %} {%- if show_source and has_source and sourcename %} {%- endif %}
{# mod: replaced RTD icons with our links #} {% if discourse %} {% endif %} {% if mattermost %} {% endif %} {% if github_url and github_version and github_folder %} {% if github_issues %} {% endif %} {% endif %}
netplan-1.0/doc/.sphinx/_templates/header.html000066400000000000000000000017741457004145200214350ustar00rootroot00000000000000 netplan-1.0/doc/.sphinx/_templates/page.html000066400000000000000000000024241457004145200211120ustar00rootroot00000000000000{% extends "furo/page.html" %} {% block footer %} {% include "footer.html" %} {% endblock footer %} {% block body -%} {% include "header.html" %} {{ super() }} {%- endblock body %} {% if meta and ((meta.discourse and discourse_prefix) or meta.relatedlinks) %} {% set furo_hide_toc_orig = furo_hide_toc %} {% set furo_hide_toc=false %} {% endif %} {% block right_sidebar %}
{% if not furo_hide_toc_orig %}
{{ _("Contents") }}
{{ toc }}
{% endif %} {% if meta and ((meta.discourse and discourse_prefix) or meta.relatedlinks) %} {% endif %}
{% endblock right_sidebar %} netplan-1.0/doc/.sphinx/requirements.txt000066400000000000000000000003471457004145200204410ustar00rootroot00000000000000breathe furo linkify-it-py lxd-sphinx-extensions myst-parser pyenchant pyspelling sphinx sphinx-autobuild sphinx-copybutton sphinx-design sphinx-notfound-page sphinx-reredirects sphinx-tabs sphinxcontrib-jquery sphinxext-opengraph netplan-1.0/doc/.sphinx/spellingcheck.yaml000066400000000000000000000011521457004145200206470ustar00rootroot00000000000000matrix: - name: rST files aspell: lang: en d: en_GB dictionary: wordlists: - .wordlist.txt - .custom_wordlist.txt output: .sphinx/.wordlist.dic sources: - _build/**/*.html|!_build/genindex/index.html|!_build/apidoc/**/*.html pipeline: - pyspelling.filters.html: comments: false attributes: - title - alt ignores: - code - pre - spellexception - link - title - div.relatedlinks - strong.command - div.visually-hidden - img - a.p-navigation__link - a.reference - article>section>h1 netplan-1.0/doc/.wokeignore000066400000000000000000000000211457004145200157250ustar00rootroot00000000000000netplan-yaml.md netplan-1.0/doc/.wordlist.txt000066400000000000000000000004511457004145200162500ustar00rootroot00000000000000addons API APIs balancer Charmhub CLI dropdown Diátaxis EBS EKS favicon Furo Grafana IAM installable JSON Juju Kubernetes Kubeflow Makefile Mattermost MyST namespace namespaces NodePort observability OLM Permalink pre ReadMe reST reStructuredText RTD subdirectories subtree subfolders UI VM YAML netplan-1.0/doc/CODE_OF_CONDUCT.md000066400000000000000000000203551457004145200163660ustar00rootroot00000000000000# Ubuntu Code of Conduct v2.0 Ubuntu is about showing humanity to one another: the word itself captures the spirit of being human. We want a productive, happy and agile community that can welcome new ideas in a complex field, improve every process every year, and foster collaboration between groups with very different needs, interests and skills. We gain strength from diversity, and actively seek participation from those who enhance it. This code of conduct exists to ensure that diverse groups collaborate to mutual advantage and enjoyment. We will challenge prejudice that could jeopardise the participation of any person in the project. The Code of Conduct governs how we behave in public or in private whenever the project will be judged by our actions. We expect it to be honoured by everyone who represents the project officially or informally, claims affiliation with the project, or participates directly. ## We strive to: * Be considerate Our work will be used by other people, and we in turn will depend on the work of others. Any decision we take will affect users and colleagues, and we should consider them when making decisions. * Be respectful Disagreement is no excuse for poor manners. We work together to resolve conflict, assume good intentions and do our best to act in an empathic fashion. We don’t allow frustration to turn into a personal attack. A community where people feel uncomfortable or threatened is not a productive one. * Take responsibility for our words and our actions We can all make mistakes; when we do, we take responsibility for them. If someone has been harmed or offended, we listen carefully and respectfully, and work to right the wrong. * Be collaborative What we produce is a complex whole made of many parts, it is the sum of many dreams. Collaboration between teams that each have their own goal and vision is essential; for the whole to be more than the sum of its parts, each part must make an effort to understand the whole.Collaboration reduces redundancy and improves the quality of our work. Internally and externally, we celebrate good collaboration. Wherever possible, we work closely with upstream projects and others in the free software community to coordinate our efforts. We prefer to work transparently and involve interested parties as early as possible. * Value decisiveness, clarity and consensus Disagreements, social and technical, are normal, but we do not allow them to persist and fester leaving others uncertain of the agreed direction.We expect participants in the project to resolve disagreements constructively. When they cannot, we escalate the matter to structures with designated leaders to arbitrate and provide clarity and direction. * Ask for help when unsure Nobody is expected to be perfect in this community. Asking questions early avoids many problems later, so questions are encouraged, though they may be directed to the appropriate forum. Those who are asked should be responsive and helpful. * Step down considerately When somebody leaves or disengages from the project, we ask that they do so in a way that minimises disruption to the project. They should tell people they are leaving and take the proper steps to ensure that others can pick up where they left off. ## Leadership, authority and responsibility We all lead by example, in debate and in action. We encourage new participants to feel empowered to lead, to take action, and to experiment when they feel innovation could improve the project. Leadership can be exercised by anyone simply by taking action, there is no need to wait for recognition when the opportunity to lead presents itself. ### Delegation from the top Responsibility for the project starts with the “benevolent dictator”, who delegates specific responsibilities and the corresponding authority to a series of teams, councils and individuals, starting with the Community Council (“CC”). That Council or its delegated representative will arbitrate in any dispute. We are a meritocracy; we delegate decision making, governance and leadership from senior bodies to the most able and engaged candidates. ### Support for delegation is measured Nominations to the boards and councils are at the discretion of the Community Council, however the Community Council will seek the input of the community before confirming appointments. Leadership is not an award, right, or title; it is a privilege, a responsibility and a mandate. A leader will only retain their authority as long as they retain the support of those who delegated that authority to them. ### We value discussion, data and decisiveness We gather opinions, data and commitments from concerned parties before taking a decision. We expect leaders to help teams come to a decision in a reasonable time, to seek guidance or be willing to take the decision themselves when consensus is lacking, and to take responsibility for implementation. The poorest decision of all is no decision: clarity of direction has value in itself. Sometimes all the data are not available, or consensus is elusive. A decision must still be made. There is no guarantee of a perfect decision every time - we prefer to err, learn, and err less in future than to postpone action indefinitely. We recognise that the project works better when we trust the teams closest to a problem to make the decision for the project. If we learn of a decision that we disagree with, we can engage the relevant team to find common ground, and failing that, we have a governance structure that can review the decision. Ultimately, if a decision has been taken by the people responsible for it, and is supported by the project governance, it will stand. None of us expects to agree with every decision, and we value highly the willingness to stand by the project and help it deliver even on the occasions when we ourselves may prefer a different route. ### Open meritocracy We invite anybody, from any company, to participate in any aspect of the project. Our community is open, and any responsibility can be carried by any contributor who demonstrates the required capacity and competence. ### Teamwork A leader’s foremost goal is the success of the team. “A virtuoso is judged by their actions; a leader is judged by the actions of their team.” A leader knows when to act and when to step back. They know when to delegate work, and when to take it upon themselves. ### Credit A good leader does not seek the limelight, but celebrates team members for the work they do. Leaders may be more visible than members of the team, good ones use that visibility to highlight the great work of others. ### Courage and considerateness Leadership occasionally requires bold decisions that will not be widely understood, consensual or popular. We value the courage to take such decisions, because they enable the project as a whole to move forward faster than we could if we required complete consensus. Nevertheless, boldness demands considerateness; take bold decisions, but do so mindful of the challenges they present for others, and work to soften the impact of those decisions on them. Communicating changes and their reasoning clearly and early on is as important as the implementation of the change itself. ### Conflicts of interest We expect leaders to be aware when they are conflicted due to employment or other projects they are involved in, and abstain or delegate decisions that may be seen to be self-interested. We expect that everyone who participates in the project does so with the goal of making life better for its users. When in doubt, ask for a second opinion. Perceived conflicts of interest are important to address; as a leader, act to ensure that decisions are credible even if they must occasionally be unpopular, difficult or favourable to the interests of one group over another. This Code is not exhaustive or complete. It is not a rulebook; it serves to distil our common understanding of a collaborative, shared environment and goals. We expect it to be followed in spirit as much as in the letter. *The Ubuntu Code of Conduct is licensed under the [Creative Commons Attribution-Share Alike 3.0 license](https://creativecommons.org/licenses/by-sa/3.0/). You may re-use it for your own project, and modify it as you wish, just please allow others to use your modifications and give credit to the Ubuntu Project!* netplan-1.0/doc/Makefile000066400000000000000000000064711457004145200152320ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= -c . -d .sphinx/.doctrees SPHINXBUILD ?= sphinx-build SPHINXDIR = .sphinx SOURCEDIR = . BUILDDIR = _build VENVDIR = $(SPHINXDIR)/venv VENV = $(VENVDIR)/bin/activate SHELL = /usr/bin/bash .PHONY: help woke-install install run html epub serve clean clean-doc \ spelling linkcheck woke Makefile # Put it first so that "make" without argument is like "make help". help: $(VENVDIR) @. $(VENV); $(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) # Explicit target avoids fall-through to the "Makefile" target. $(SPHINXDIR)/requirements.txt: test -f $(SPHINXDIR)/requirements.txt # If requirements are updated, venv should be rebuilt and timestamped. $(VENVDIR): $(SPHINXDIR)/requirements.txt @echo "... setting up virtualenv" python3 -m venv $(VENVDIR) . $(VENV); pip install --require-virtualenv \ --upgrade -r $(SPHINXDIR)/requirements.txt \ --log $(VENVDIR)/pip_install.log @test ! -f $(VENVDIR)/pip_list.txt || \ mv $(VENVDIR)/pip_list.txt $(VENVDIR)/pip_list.txt.bak @. $(VENV); pip list --local --format=freeze > $(VENVDIR)/pip_list.txt @echo "\n" \ "--------------------------------------------------------------- \n" \ "* watch, build and serve the documentation: make run \n" \ "* only build: make html \n" \ "* only serve: make serve \n" \ "* clean built doc files: make clean-doc \n" \ "* clean full environment: make clean \n" \ "* check links: make linkcheck \n" \ "* check spelling: make spelling \n" \ "* check inclusive language: make woke \n" \ "* other possible targets: make \n" \ "--------------------------------------------------------------- \n" @touch $(VENVDIR) woke-install: @type woke >/dev/null 2>&1 || \ { echo "Installing \"woke\" snap... \n"; sudo snap install woke; } install: $(VENVDIR) woke-install run: install . $(VENV); sphinx-autobuild -b dirhtml "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) --re-ignore '\.sphinx/breathe/doxygen/.*' # Doesn't depend on $(BUILDDIR) to rebuild properly at every run. html: install . $(VENV); $(SPHINXBUILD) -b dirhtml "$(SOURCEDIR)" "$(BUILDDIR)" -w .sphinx/warnings.txt $(SPHINXOPTS) epub: install . $(VENV); $(SPHINXBUILD) -b epub "$(SOURCEDIR)" "$(BUILDDIR)" -w .sphinx/warnings.txt $(SPHINXOPTS) serve: html cd "$(BUILDDIR)"; python3 -m http.server 8000 clean: clean-doc @test ! -e "$(VENVDIR)" -o -d "$(VENVDIR)" -a "$(abspath $(VENVDIR))" != "$(VENVDIR)" rm -rf $(VENVDIR) rm -rf .sphinx/.doctrees clean-doc: git clean -fx "$(BUILDDIR)" spelling: html . $(VENV) ; python3 -m pyspelling -c .sphinx/spellingcheck.yaml linkcheck: install . $(VENV) ; $(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) woke: woke-install woke *.{md,c,py} **/*.{md,c,py} --exit-1-on-failure \ -c https://github.com/canonical/Inclusive-naming/raw/main/config.yml # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile . $(VENV); $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) netplan-1.0/doc/apidoc/000077500000000000000000000000001457004145200150215ustar00rootroot00000000000000netplan-1.0/doc/apidoc/inc-netdef.md000066400000000000000000000000661457004145200173610ustar00rootroot00000000000000# netdef.h ```{autodoxygenfile} include/netdef.h ``` netplan-1.0/doc/apidoc/inc-parse-nm.md000066400000000000000000000000721457004145200176330ustar00rootroot00000000000000# parse-nm.h ```{autodoxygenfile} include/parse-nm.h ``` netplan-1.0/doc/apidoc/inc-parse.md000066400000000000000000000000641457004145200172240ustar00rootroot00000000000000# parse.h ```{autodoxygenfile} include/parse.h ``` netplan-1.0/doc/apidoc/inc-state.md000066400000000000000000000000641457004145200172320ustar00rootroot00000000000000# state.h ```{autodoxygenfile} include/state.h ``` netplan-1.0/doc/apidoc/inc-types.md000066400000000000000000000000641457004145200172560ustar00rootroot00000000000000# types.h ```{autodoxygenfile} include/types.h ``` netplan-1.0/doc/apidoc/inc-util.md000066400000000000000000000000621457004145200170650ustar00rootroot00000000000000# util.h ```{autodoxygenfile} include/util.h ``` netplan-1.0/doc/apidoc/index.md000066400000000000000000000014271457004145200164560ustar00rootroot00000000000000# Reference: libnetplan API ## Public headers ```{toctree} --- maxdepth: 1 --- inc-parse ``` > ```{autodoxygenfile} include/parse.h > :sections: briefdescription > ``` ```{toctree} --- maxdepth: 1 --- inc-parse-nm ``` > ```{autodoxygenfile} include/parse-nm.h > :sections: briefdescription > ``` ```{toctree} --- maxdepth: 1 --- inc-state ``` > ```{autodoxygenfile} include/state.h > :sections: briefdescription > ``` ```{toctree} --- maxdepth: 1 --- inc-netdef ``` > ```{autodoxygenfile} include/netdef.h > :sections: briefdescription > ``` ```{toctree} --- maxdepth: 1 --- inc-util ``` > ```{autodoxygenfile} include/util.h > :sections: briefdescription > ``` ```{toctree} --- maxdepth: 1 --- inc-types ``` > ```{autodoxygenfile} include/types.h > :sections: briefdescription > ``` netplan-1.0/doc/cli.md000066400000000000000000000024511457004145200146550ustar00rootroot00000000000000# Netplan CLI ```{toctree} --- maxdepth: 1 hidden: true --- generate apply try get set info ip rebind status ``` Netplan provides a command line interface called `netplan`, which a user can use to control certain aspects of the Netplan configuration. | Tool | Description | | --- | --- | | help | Show a generic help message | | [generate](/netplan-generate) | Generate back-end specific configuration files from `/etc/netplan/*.yaml` | | [apply](/netplan-apply) | Apply current Netplan configuration to running system | | [try](/netplan-try) | Try to apply a new Netplan configuration to running system, with automatic rollback | | [get](/netplan-get) | Get a setting by specifying a nested key like `"ethernets.eth0.addresses"`, or `"all"` | | [set](/netplan-set) | Add new setting by specifying a dotted `key=value` pair like `"ethernets.eth0.dhcp4=true"` | | [info](/netplan-info) | Show available features | | [ip](/netplan-ip) | Retrieve IP information (like DHCP leases) from the system | | [rebind](/netplan-rebind) | Rebind SR-IOV virtual functions of given physical functions to their driver | | [status](/netplan-status) | Query networking state of the running system | netplan-1.0/doc/conf.py000066400000000000000000000315161457004145200150670ustar00rootroot00000000000000import sys import datetime import atexit import fileinput import os import re # Custom configuration for the Sphinx documentation builder. # All configuration specific to your project should be done in this file. # # The file is included in the common conf.py configuration file. # You can modify any of the settings below or add any configuration that # is not covered by the common conf.py file. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/ # # If you're not familiar with Sphinx and don't want to use advanced # features, it is sufficient to update the settings in the "Project # information" section. ############################################################ # Project information ############################################################ # Product name project = 'Netplan' author = 'Netplan team' # The title you want to display for the documentation in the sidebar. # You might want to include a version number here. # To not display any title, set this option to an empty string. html_title = project + ' documentation' # The default value uses the current year as the copyright year. # # For static works, it is common to provide the year of first publication. # Another option is to give the first year and the current year # for documentation that is often changed, e.g. 2022–2023 (note the en-dash). # # A way to check a GitHub repo's creation date is to obtain a classic GitHub # token with 'repo' permissions here: https://github.com/settings/tokens # Next, use 'curl' and 'jq' to extract the date from the GitHub API's output: # # curl -H 'Authorization: token ' \ # -H 'Accept: application/vnd.github.v3.raw' \ # https://api.github.com/repos/canonical/ | jq '.created_at' copyright = '%s, %s' % (datetime.date.today().year, author) # Open Graph configuration - defines what is displayed as a link preview # when linking to the documentation from another website (see https://ogp.me/) # The URL where the documentation will be hosted (leave empty if you # don't know yet) ogp_site_url = 'https://netplan.readthedocs.io/en/stable/netplan-apply/' # The documentation website name (usually the same as the product name) ogp_site_name = project # The URL of an image or logo that is used in the preview ogp_image = 'https://assets.ubuntu.com/v1/253da317-image-document-ubuntudocs.svg' # Update with the local path to the favicon for your product # (default is the circle of friends) html_favicon = '.sphinx/_static/favicon.png' # The logo html_logo = '.sphinx/_static/netplan.svg' # (Some settings must be part of the html_context dictionary, while others # are on root level. Don't move the settings.) html_context = { # Change to the link to the website of your product (without "https://") # For example: "ubuntu.com/lxd" or "microcloud.is" # If there is no product website, edit the header template to remove the # link (see the readme for instructions). 'product_page': 'netplan.io', # Add your product tag (the orange part of your logo, will be used in the # header) to ".sphinx/_static" and change the path here (start with "_static") # (default is the circle of friends) 'product_tag': '_static/tag.png', # Change to the discourse instance you want to be able to link to # using the :discourse: metadata at the top of a file # (use an empty value if you don't want to link) 'discourse': 'https://discourse.ubuntu.com/c/foundations/', # Change to the Mattermost channel you want to link to # (use an empty value if you don't want to link) 'mattermost': 'https://chat.canonical.com/canonical/channels/documentation', # Change to the GitHub URL for your project 'github_url': 'https://github.com/canonical/netplan', # Change to the branch for this version of the documentation 'github_version': 'main', # Change to the folder that contains the documentation # (usually "/" or "/docs/") 'github_folder': '/doc/', # Change to an empty value if your GitHub repo doesn't have issues enabled. # This will disable the feedback button and the issue link in the footer. 'github_issues': 'https://bugs.launchpad.net/netplan/', # Controls the existence of Previous / Next buttons at the bottom of pages # Valid options: none, prev, next, both 'sequential_nav': "none" } # If your project is on documentation.ubuntu.com, specify the project # slug (for example, "lxd") here. slug = "" ############################################################ # Redirects ############################################################ # Set up redirects (https://documatt.gitlab.io/sphinx-reredirects/usage.html) # For example: 'explanation/old-name.html': '../how-to/prettify.html', redirects = { 'README.md': '/', 'netplan': '/netplan-yaml', } ############################################################ # Link checker exceptions ############################################################ # Links to ignore when checking links linkcheck_ignore = [ 'http://127.0.0.1:8000' ] # Pages on which to ignore anchors # (This list will be appended to linkcheck_anchors_ignore_for_url) custom_linkcheck_anchors_ignore_for_url = [] ############################################################ # Additions to default configuration ############################################################ # The following settings are appended to the default configuration. # Use them to extend the default functionality. # Add extensions custom_extensions = [ 'breathe', ] # Add MyST extensions custom_myst_extensions = ["colon_fence"] # Add files or directories that should be excluded from processing. custom_excludes = [ 'doc-cheat-sheet*', 'manpage-footer.md', 'manpage-header.md', 'CODE_OF_CONDUCT.md', ] # Add CSS files (located in .sphinx/_static/) custom_html_css_files = [] # Add JavaScript files (located in .sphinx/_static/) custom_html_js_files = [] # The following settings override the default configuration. # Specify a reST string that is included at the end of each file. # If commented out or empty, use the default (which pulls the reuse/links.txt # file into each reST file). custom_rst_epilog = '' # By default, the documentation includes a feedback button at the top. # You can disable it by setting the following configuration to True. disable_feedback_button = False # Add tags that you want to use for conditional inclusion of text # (https://www.sphinx-doc.org/) custom_tags = [] ############################################################ # Additional configuration ############################################################ # Add any configuration that is not covered by the common conf.py file. smartquotes_action = 'qe' # Doxygen # https://breathe.readthedocs.io/en/latest/directives.html # breathe_projects = {"Netplan": "../doxyxml/"} breathe_projects_source = {"auto-apidoc": ("../", [ "include/netdef.h", "include/netplan.h", "include/parse-nm.h", "include/parse.h", "include/state.h", "include/types.h", "include/util.h", # "src/error.c", # "src/names.c", # "src/netplan.c", # "src/parse-nm.c", # "src/parse.c", # "src/types.c", # "src/util.c", # "src/validation.c", ])} breathe_doxygen_config_options = { 'MACRO_EXPANSION': 'YES', 'EXPAND_ONLY_PREDEF': 'YES', 'PREDEFINED': 'NETPLAN_PUBLIC NETPLAN_DEPRECATED', } breathe_domain_by_extension = { "h": "c", "c": "c", } # breathe_doxygen_aliases = breathe_default_project = "auto-apidoc" # Options for MyST myst_title_to_header = True suppress_warnings = ['myst.xref_missing'] # # sys.path.append('./') # # from custom_conf import * # Configuration file for the Sphinx documentation builder. # You should not do any modifications to this file. Put your custom # configuration into the custom_conf.py file. # If you need to change this file, contribute the changes upstream. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/ ############################################################ # Extensions ############################################################ extensions = [ 'sphinx_design', 'sphinx_tabs.tabs', 'sphinx_reredirects', 'youtube-links', 'related-links', 'custom-rst-roles', 'terminal-output', 'sphinx_copybutton', 'sphinxext.opengraph', 'myst_parser', 'sphinxcontrib.jquery', 'notfound.extension' ] extensions.extend(custom_extensions) # Configuration for extensions # Additional MyST syntax myst_enable_extensions = [ 'substitution', 'deflist', 'linkify' ] myst_enable_extensions.extend(custom_myst_extensions) # Used for related links if 'discourse_prefix' not in html_context and 'discourse' not in html_context: html_context['discourse_prefix'] = html_context['discourse'] + '/t/' # The default for notfound_urls_prefix usually works, but not for # documentation on documentation.ubuntu.com if slug: notfound_urls_prefix = '/' + slug + '/en/latest/' notfound_context = { 'title': 'Page not found', 'body': '

Page not found

\n\n

Sorry, but the documentation page \ that you are looking for was not found.

\n

Documentation changes \ over time, and pages are moved around. We try to redirect you to the \ updated content where possible, but unfortunately, that didn\'t work \ this time (maybe because the content you were looking for does not \ exist in this version of the documentation).

\n

You can try to use \ the navigation to locate the content you\'re looking for, or search for \ a similar page.

\n', } # Default image for OGP (to prevent font errors, see # https://github.com/canonical/sphinx-docs-starter-pack/pull/54 ) if 'ogp_image' not in locals(): ogp_image = 'https://assets.ubuntu.com/v1/253da317-image-document-ubuntudocs.svg' ############################################################ # General configuration ############################################################ exclude_patterns = [ '_build', 'Thumbs.db', '.DS_Store', '.sphinx', ] exclude_patterns.extend(custom_excludes) rst_epilog = ''' .. include:: /reuse/links.txt ''' if 'custom_rst_epilog' in locals() and 'custom_rst_epilog' != '': rst_epilog = custom_rst_epilog source_suffix = { '.rst': 'restructuredtext', '.md': 'markdown', } if 'conf_py_path' not in html_context and 'github_folder' not in html_context: html_context['conf_py_path'] = html_context['github_folder'] # For ignoring specific links linkcheck_anchors_ignore_for_url = [ r'https://github\.com/.*' ] linkcheck_anchors_ignore_for_url.extend(custom_linkcheck_anchors_ignore_for_url) # Tags cannot be added directly in custom_conf.py, so add them here tags = () for tag in custom_tags: tags.add(tag) ############################################################ # Styling ############################################################ # Find the current builder builder = 'dirhtml' if '-b' in sys.argv: builder = sys.argv[sys.argv.index('-b')+1] # Setting templates_path for epub makes the build fail if builder == 'dirhtml' or builder == 'html': templates_path = ['.sphinx/_templates'] # Theme configuration html_theme = 'furo' html_last_updated_fmt = '' html_permalinks_icon = '¶' if html_title == '': html_theme_options = { 'sidebar_hide_name': True } ############################################################ # Additional files ############################################################ html_static_path = ['.sphinx/_static'] html_css_files = [ 'custom.css', 'header.css', 'github_issue_links.css', 'furo_colors.css' ] html_css_files.extend(custom_html_css_files) html_js_files = ['header-nav.js'] if 'github_issues' in html_context and html_context['github_issues'] and not disable_feedback_button: html_js_files.append('github_issue_links.js') html_js_files.extend(custom_html_js_files) # Clip the "How to" string from how-to titles in the left navigation sidebar # and capitalizes the first letter of the remaining title. def clip_howto(): pattern = re.compile(r'(toctree-l2.*>)How to ([a-zA-Z])') # Check if the build is running on ReadTheDocs if os.environ.get('READTHEDOCS') == 'True': output_dir = os.environ.get('READTHEDOCS_OUTPUT') + '/html' else: output_dir = '_build' print("Clipping 'How to' from the Table of Contents...") for root, dirs, files in os.walk(output_dir): for file in files: if file.endswith('.html'): file_path = os.path.join(root, file) with fileinput.FileInput(file_path, inplace=True) as f: for line in f: line = pattern.sub(lambda match: match.group(1) + match.group(2).upper(), line) print(line, end='') # Register the clip_howto function to be called on exit atexit.register(clip_howto) # End how-to clipping netplan-1.0/doc/contribute-docs.md000066400000000000000000000021061457004145200172070ustar00rootroot00000000000000# How to contribute documentation ## Reporting an issue If you find any issue in Netplan documentation please [file a bugreport](https://bugs.launchpad.net/netplan/+filebug?field.tags=documentation) about it in our bug tracker on Launchpad. Remember adding a `documentation` tag to it. ## Modifying documentation online Each documentation page rendered on the web contains an **Edit this page** link in the top-right of every page. Clicking this button will lead you to the GitHub web editor where you can propose changes to the corresponding page. Please remember to first check the [latest version](https://netplan.readthedocs.io/en/latest/) of our documentation and make your proposal based on that revision. ## Creating a pull request If you want to follow a Git development workflow, you can also checkout the [Netplan repository](https://github.com/canonical/netplan) and contribute your changes as [pull requests](https://github.com/canonical/netplan/pulls), putting the `documentation` label for better visibility. See the `doc/` and `examples/` directories for relevant files. netplan-1.0/doc/dbus-config.md000066400000000000000000000047551457004145200163170ustar00rootroot00000000000000# How to use D-Bus configuration API See also: * [Netplan D-Bus reference](/netplan-dbus) * [`busctl` reference](https://www.freedesktop.org/software/systemd/man/busctl.html) Copy the current state from `/{etc,run,lib}/netplan/*.yaml` by creating a new configuration object: ```console busctl call io.netplan.Netplan /io/netplan/Netplan io.netplan.Netplan Config o "/io/netplan/Netplan/config/ULJIU0" ``` Read the merged YAML configuration: ```console busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 \ io.netplan.Netplan.Config Get s "network:\n ethernets:\n eth0:\n dhcp4: true\n renderer: networkd\n version: 2\n" ``` Write a new configuration snippet into `70-snapd.yaml`: ```console busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 \ io.netplan.Netplan.Config Set ss "ethernets.eth0={dhcp4: false, dhcp6: true}" "70-snapd" b true ``` Check the newly written configuration: ```console busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 \ io.netplan.Netplan.Config Get s "network:\n ethernets:\n eth0:\n dhcp4: false\n dhcp6: true\n renderer: networkd\n version: 2\n" ``` Try to apply the current state of the configuration object: ```console busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 \ io.netplan.Netplan.Config Try u 20 b true ``` Accept the `Try()` state within the 20 seconds timeout, if not it will be auto-rejected: ```console busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 \ io.netplan.Netplan.Config Apply b true [SIGNAL] io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 io.netplan.Netplan.Config Changed() is triggered [OBJECT] io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 is removed from the bus ``` Create a new configuration object and get the merged YAML configuration: ```console busctl call io.netplan.Netplan /io/netplan/Netplan io.netplan.Netplan Config o "/io/netplan/Netplan/config/KC0IU0 busctl call io.netplan.Netplan /io/netplan/Netplan/config/KC0IU0 \ io.netplan.Netplan.Config Get s "network:\n ethernets:\n eth0:\n dhcp4: false\n dhcp6: true\n renderer: networkd\n version: 2\n" ``` Reject that configuration object again: ```console busctl call io.netplan.Netplan /io/netplan/Netplan/config/KC0IU0 \ io.netplan.Netplan.Config Cancel b true [SIGNAL] io.netplan.Netplan /io/netplan/Netplan/config/KC0IU0 io.netplan.Netplan.Config Changed() is triggered [OBJECT] io.netplan.Netplan /io/netplan/Netplan/config/KC0IU0 is removed from the bus ``` netplan-1.0/doc/example-config000066400000000000000000000035221457004145200164050ustar00rootroot00000000000000network: version: 2 # if specified, can only realistically have that value, as networkd cannot # render wifi/3G. This would be shipped as a separate snippet by desktop images. #renderer: NetworkManager ethernets: # opaque ID for physical interfaces, only referred to by other stanzas id0: match: macaddress: 00:11:22:33:44:55 wakeonlan: true dhcp4: true addresses: - 192.168.14.2/24 - "2001:1::1/64" routes: - to: default via: 192.168.14.1 - to: default via: "2001:1::2" - to: 11.22.0.0/16 via: 192.168.14.3 metric: 100 nameservers: search: [foo.local, bar.local] addresses: [8.8.8.8] lom: match: driver: ixgbe # you are responsible for setting tight enough match rules # that only match one device if you use set-name set-name: lom1 dhcp6: true switchports: # all cards on second PCI bus; unconfigured by themselves, will be added # to br0 below (note: globbing is not supported by NetworkManager) match: name: enp2* mtu: 1280 wifis: all-wlans: # useful on a system where you know there is only ever going to be one device match: {} access-points: "Joe's home": # mode defaults to "managed" (client) password: "s3kr1t" # this creates an AP on wlp1s0 using hostapd; no match rules, thus ID is # the interface name wlp1s0: access-points: "guest": mode: ap # no WPA config implies default of open bridges: # the key name is the name for virtual (created) interfaces; no match: and # set-name: allowed br0: # IDs of the components; switchports expands into multiple interfaces interfaces: [wlp1s0, switchports] dhcp4: true netplan-1.0/doc/examples.md000066400000000000000000000365271457004145200157370ustar00rootroot00000000000000# How to enable DHCP on an interface To let the interface named `enp3s0` get an address via DHCP, create a YAML file with the following: ```yaml network: version: 2 renderer: networkd ethernets: enp3s0: dhcp4: true ``` # How to configure a static IP address on an interface To set a static IP address, use the `addresses` keyword, which takes a list of (IPv4 or IPv6) addresses along with the subnet prefix length (e.g. /24). ```yaml network: version: 2 renderer: networkd ethernets: enp3s0: addresses: - 10.10.10.2/24 ``` # How to configure DNS servers and search domains The lists of search domains and DNS server IP addresses can be defined as below: ```yaml network: version: 2 renderer: networkd ethernets: enp3s0: addresses: - 10.10.10.2/24 nameservers: search: - "mycompany.local" addresses: - 10.10.10.253 - 8.8.8.8 ``` # How to connect multiple interfaces with DHCP DHCP can be used with multiple interfaces. The metrics for the routes acquired from DHCP can be changed with the use of DHCP overrides. In this example, `enp5s0` is preferred over `enp6s0`, as it has a lower route metric: ```yaml network: version: 2 ethernets: enp5s0: dhcp4: yes dhcp4-overrides: route-metric: 100 enp6s0: dhcp4: yes dhcp4-overrides: route-metric: 200 ``` # How to connect to an open wireless network For open wireless networks, Netplan only requires that the access point is defined. In this example, `opennetwork` is the network SSID: ```yaml network: version: 2 wifis: wl0: access-points: opennetwork: {} dhcp4: yes ``` # How to configure your computer to connect to your home Wi-Fi network If all you need is to connect to your local domestic Wi-Fi network, use the configuration below: ```yaml network: version: 2 renderer: NetworkManager wifis: wlp2s0b1: dhcp4: yes access-points: "network_ssid_name": password: "**********" ``` # How to connect to a WPA Personal wireless network without DHCP For private wireless networks, the access point name and password must be specified: ```yaml network: version: 2 renderer: networkd wifis: wlp2s0b1: dhcp4: no dhcp6: no addresses: [192.168.0.21/24] nameservers: addresses: [192.168.0.1, 8.8.8.8] access-points: "network_ssid_name": password: "**********" routes: - to: default via: 192.168.0.1 ``` # How to connect to WPA Enterprise wireless networks with EAP+TTLS ```yaml network: version: 2 wifis: wl0: access-points: workplace: auth: key-management: eap method: ttls anonymous-identity: "@internal.example.com" identity: "joe@internal.example.com" password: "v3ryS3kr1t" dhcp4: yes ``` # How to connect to WPA Enterprise wireless networks with EAP+TLS ```yaml network: version: 2 wifis: wl0: access-points: university: auth: key-management: eap method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "d3cryptPr1v4t3K3y" dhcp4: yes ``` Many different modes of encryption are supported. See the [Netplan reference](/reference) page. # How to use multiple addresses on a single interface The `addresses` keyword can take a list of addresses to assign to an interface. You can also defined a `label` for each address: ```yaml network: version: 2 renderer: networkd ethernets: enp3s0: addresses: - 10.100.1.37/24 - 10.100.1.38/24: label: "enp3s0:0" - 10.100.1.39/24: label: "enp3s0:some-label" ``` # How to use multiple addresses with multiple gateways Similar to the example above, interfaces with multiple addresses can be configured with multiple gateways. ```yaml network: version: 2 renderer: networkd ethernets: enp3s0: addresses: - 10.0.0.10/24 - 11.0.0.11/24 routes: - to: default via: 10.0.0.1 metric: 200 - to: default via: 11.0.0.1 metric: 300 ``` We configure individual routes to default (or 0.0.0.0/0) using the address of the gateway for the subnet. The `metric` value should be adjusted so the routing happens as expected. DHCP can be used to receive one of the IP addresses for the interface. In this case, the default route for that address will be automatically configured with a `metric` value of 100. # How to use NetworkManager as a renderer Netplan supports both `networkd` and NetworkManager as back ends. You can specify which network back end should be used to configure particular devices by using the `renderer` key. You can also delegate all configuration of the network to NetworkManager itself by specifying only the `renderer` key: ```yaml network: version: 2 renderer: NetworkManager ``` # How to configure interface bonding Bonding is configured by declaring a bond interface with a list of physical interfaces and a bonding mode: ```yaml network: version: 2 renderer: networkd bonds: bond0: dhcp4: yes interfaces: - enp3s0 - enp4s0 parameters: mode: active-backup primary: enp3s0 ``` # How to configure multiple bonds Below is an example of a system acting as a router with various bonded interfaces and different types. Note the 'optional: true' key declarations that allow booting to occur without waiting for those interfaces to activate fully. ```yaml network: version: 2 renderer: networkd ethernets: enp1s0: dhcp4: no enp2s0: dhcp4: no enp3s0: dhcp4: no optional: true enp4s0: dhcp4: no optional: true enp5s0: dhcp4: no optional: true enp6s0: dhcp4: no optional: true bonds: bond-lan: interfaces: [enp2s0, enp3s0] addresses: [192.168.93.2/24] parameters: mode: 802.3ad mii-monitor-interval: 1 bond-wan: interfaces: [enp1s0, enp4s0] addresses: [192.168.1.252/24] nameservers: search: [local] addresses: [8.8.8.8, 8.8.4.4] parameters: mode: active-backup mii-monitor-interval: 1 gratuitious-arp: 5 routes: - to: default via: 192.168.1.1 bond-conntrack: interfaces: [enp5s0, enp6s0] addresses: [192.168.254.2/24] parameters: mode: balance-rr mii-monitor-interval: 1 ``` # How to configure network bridges Use the following configuration to create a simple bridge consisting of a single device that uses DHCP: ```yaml network: version: 2 renderer: networkd ethernets: enp3s0: dhcp4: no bridges: br0: dhcp4: yes interfaces: - enp3s0 ``` # How to create a bridge with a VLAN for libvirtd To get libvirtd to use a specific bridge with a tagged VLAN, while continuing to provide an untagged interface as well would involve: ```yaml network: version: 2 renderer: networkd ethernets: enp0s25: dhcp4: true bridges: br0: addresses: [ 10.3.99.25/24 ] interfaces: [ vlan15 ] vlans: vlan15: accept-ra: no id: 15 link: enp0s25 ``` Then libvirtd would be configured to use this bridge by adding the following content to a new XML file under `/etc/libvirt/qemu/networks/`. The name of the bridge in the <bridge> tag as well as in <name> need to match the name of the bridge device configured using Netplan: ```xml br0 ``` # How to create VLANs To configure multiple VLANs with renamed interfaces: ```yaml network: version: 2 renderer: networkd ethernets: mainif: match: macaddress: "de:ad:be:ef:ca:fe" set-name: mainif addresses: [ "10.3.0.5/23" ] nameservers: addresses: [ "8.8.8.8", "8.8.4.4" ] search: [ example.com ] routes: - to: default via: 10.3.0.1 vlans: vlan15: id: 15 link: mainif addresses: [ "10.3.99.5/24" ] vlan10: id: 10 link: mainif addresses: [ "10.3.98.5/24" ] nameservers: addresses: [ "127.0.0.1" ] search: [ domain1.example.com, domain2.example.com ] ``` # How to use a directly connected gateway This allows setting up a default route, or any route, using the "on-link" keyword where the gateway is an IP address that is directly connected to the network even if the address does not match the subnet configured on the interface. ```yaml network: version: 2 renderer: networkd ethernets: ens3: addresses: [ "10.10.10.1/24" ] routes: - to: default # or 0.0.0.0/0 via: 9.9.9.9 on-link: true ``` For IPv6 the configuration would be very similar: ```yaml network: version: 2 renderer: networkd ethernets: ens3: addresses: [ "2001:cafe:face:beef::dead:dead/64" ] routes: - to: default # or "::/0" via: "2001:cafe:face::1" on-link: true ``` # How to configure source routing In the example below, ens3 is on the 192.168.3.0/24 network and ens5 is on the 192.168.5.0/24 network. This enables clients on either network to connect to the other and allow the response to come from the correct interface. Furthermore, the default route is still assigned to ens5 allowing any other traffic to go through it. ```yaml network: version: 2 renderer: networkd ethernets: ens3: addresses: - 192.168.3.30/24 dhcp4: no routes: - to: 192.168.3.0/24 via: 192.168.3.1 table: 101 routing-policy: - from: 192.168.3.0/24 table: 101 ens5: addresses: - 192.168.5.24/24 dhcp4: no routes: - to: default via: 192.168.5.1 - to: 192.168.5.0/24 via: 192.168.5.1 table: 102 routing-policy: - from: 192.168.5.0/24 table: 102 ``` # How to configure a loopback interface `networkd` does not allow creating new loopback devices, but a user can add new addresses to the standard loopback interface, `lo`, in order to have it considered a valid address on the machine as well as for custom routing: ```yaml network: version: 2 renderer: networkd ethernets: lo: addresses: [ "127.0.0.1/8", "::1/128", "7.7.7.7/32" ] ``` # How to integrate with Windows DHCP Server For networks where DHCP is provided by a Windows Server using the `dhcp-identifier` keyword allows for interoperability: ```yaml network: version: 2 ethernets: enp3s0: dhcp4: yes dhcp-identifier: mac ``` # How to connect to an IPv6 over IPv4 tunnel Here, 1.1.1.1 is the client's own IP address; 2.2.2.2 is the remote server's IPv4 address, "2001:dead:beef::2/64" is the client's IPv6 address as defined by the tunnel, and "2001:dead:beef::1" is the remote server's IPv6 address. Finally, "2001:cafe:face::1/64" is an address for the client within the routed IPv6 prefix: ```yaml network: version: 2 ethernets: eth0: addresses: - 1.1.1.1/24 - "2001:cafe:face::1/64" routes: - to: default via: 1.1.1.254 tunnels: he-ipv6: mode: sit remote: 2.2.2.2 local: 1.1.1.1 addresses: - "2001:dead:beef::2/64" routes: - to: default via: "2001:dead:beef::1" ``` # How to configure SR-IOV Virtual Functions For SR-IOV network cards, it is possible to dynamically allocate Virtual Function interfaces for every configured Physical Function. In Netplan, a VF is defined by having a link: property pointing to the parent PF. ```yaml network: version: 2 ethernets: eno1: mtu: 9000 enp1s16f1: link: eno1 addresses : [ "10.15.98.25/24" ] vf1: match: name: enp1s16f[2-3] link: eno1 addresses : [ "10.15.99.25/24" ] ``` # How to connect two systems with a WireGuard VPN Generate the private and public keys in the first peer. Run the following commands with administrator privileges: ```console wg genkey > private.key wg pubkey < private.key > public.key cat private.key UMjI9WbobURkCDh2RT8SRM5osFI7siiR/sPOuuTIDns= cat public.key EdNnZ1/2OJZ9HcScSVcwDVUsctCkKQ/xzjEyd3lZFFs= ``` Do the same in the second peer: ```console wg genkey > private.key wg pubkey < private.key > public.key cat private.key UAmjvLDVuV384OWFJkmI4bG8AIAZAfV7LarshnV3+lc= cat public.key AIm+QeCoC23zInKASmhu6z/3iaT0R2IKraB7WwYB5ms= ``` Use the following configuration in the `first peer` (replace the keys and IP addresses as needed): ```yaml network: tunnels: wg0: mode: wireguard port: 51820 key: UMjI9WbobURkCDh2RT8SRM5osFI7siiR/sPOuuTIDns= addresses: - 172.16.0.1/24 peers: - allowed-ips: [172.16.0.0/24] endpoint: 10.86.126.56:51820 keys: public: AIm+QeCoC23zInKASmhu6z/3iaT0R2IKraB7WwYB5ms= ``` In the YAML file above, `key` is the first peer's `private key` and `public` is the second peer's `public key`. `endpoint` is the `second peer` IP address. Use the following configuration in the `second peer`: ```yaml network: tunnels: wg0: mode: wireguard port: 51820 key: UAmjvLDVuV384OWFJkmI4bG8AIAZAfV7LarshnV3+lc= addresses: - 172.16.0.2/24 peers: - allowed-ips: [172.16.0.0/24] endpoint: 10.86.126.40:51820 keys: public: EdNnZ1/2OJZ9HcScSVcwDVUsctCkKQ/xzjEyd3lZFFs= ``` In the YAML file above, `key` is the second peer's `private key` and `public` is the first peer's `public key`. `endpoint` is the `first peer's` IP address. # How to connect your home computer to a cloud instance with a WireGuard VPN Follow the same steps from the previous how-to to generate the necessary keys. The difference here is that your computer is likely behind one or more devices doing NAT so you probably don't have a static public IP to use as endpoint in the remote system. Use the following configuration in your computer: ```yaml network: tunnels: wg0: mode: wireguard port: 51821 key: UMjI9WbobURkCDh2RT8SRM5osFI7siiR/sPOuuTIDns= addresses: - 172.17.0.1/24 peers: - allowed-ips: [172.17.0.0/24] endpoint: 54.234.x.y:51821 keys: public: AIm+QeCoC23zInKASmhu6z/3iaT0R2IKraB7WwYB5ms= ``` Again, `key` is your private key and `public` is the remote system's public key. The `endpoint` is the public IP address of your instance. In the remote instance you just need to omit the `endpoint`. ```yaml network: tunnels: wg0: mode: wireguard port: 51821 key: UAmjvLDVuV384OWFJkmI4bG8AIAZAfV7LarshnV3+lc= addresses: - 172.17.0.2/24 peers: - allowed-ips: [172.17.0.0/24] keys: public: EdNnZ1/2OJZ9HcScSVcwDVUsctCkKQ/xzjEyd3lZFFs= ``` Don't forget to allow the UDP port `51821` in your instance's security group. After applying your configuration you should be able to reach your remote instance through the IP address `172.17.0.2`. netplan-1.0/doc/explanation.md000066400000000000000000000006031457004145200164250ustar00rootroot00000000000000# Explanation ## General structure & IDs ```{toctree} structure-id ``` ## NetworkManager ```{toctree} nm-all ``` ## Design Network configuration abstraction via `systemd-generator` ```{toctree} Netplan Design ``` ## Security ```{toctree} security ``` ## FAQs Find answers to common questions ```{toctree} Netplan FAQs ``` netplan-1.0/doc/howto.md000066400000000000000000000016441457004145200152510ustar00rootroot00000000000000# How-to guides Below is a collection of how-to guides for common scenarios. If you see a scenario missing or have one to contribute, please, [file a bug](https://bugs.launchpad.net/netplan/+filebug) against this documentation with the example. To configure Netplan, save configuration files in the `/etc/netplan/` directory with a `.yaml` extension (e.g. `/etc/netplan/config.yaml`), then run `sudo netplan apply`. This command parses and applies the configuration to the system. Configuration written to disk under `/etc/netplan/` persists between reboots. For each of the example below, use the `renderer` that applies to your scenario. For example, for Ubuntu Desktop the `renderer` is usually `NetworkManager`, and `networkd` for Ubuntu Server. Also, see [/examples](https://github.com/canonical/netplan/tree/main/examples) on GitHub. ```{toctree} :maxdepth: 1 examples dbus-config netplan-everywhere contribute-docs ``` netplan-1.0/doc/index.md000066400000000000000000000043301457004145200152130ustar00rootroot00000000000000# Netplan documentation ```{toctree} --- maxdepth: 2 hidden: true --- tutorial howto reference explanation ``` **Netplan** is a network configuration abstraction renderer. It is a **utility for network configuration** on a Linux system. You create a description of the required interfaces and define what each should do. Netplan meets the need of **easy, descriptive network configuration** in YAML across a versatile set of server, desktop, cloud or IoT installations. It is useful for **administrators of a Linux system** who want to use a common network configuration, controlling different back ends like NetworkManager or systemd-networkd. ## In this documentation ::::{grid} 1 1 2 2 :::{grid-item-card} **[Tutorial](/netplan-tutorial)** :link: /netplan-tutorial :link-type: doc **Get started** - hands-on introduction to Netplan for new users ::: :::{grid-item-card} **[How-to guides](/examples)** :link: /examples :link-type: doc **Step-by-step guides** covering key operations and common tasks ::: :::: ::::{grid} 1 1 2 2 :reverse: :::{grid-item-card} **[Reference](/reference)** :link: /reference :link-type: doc **Technical information** - specifications, APIs, architecture ::: :::{grid-item-card} **[Explanation](/explanation)** :link: /explanation :link-type: doc **Discussion and clarification** of key topics ::: :::: ## Project and community Netplan is a member of the Ubuntu family. It’s an open source project that warmly welcomes community contributions, suggestions, fixes and constructive feedback. * **[Read our code of conduct](https://ubuntu.com/community/ethos/code-of-conduct)**: As a community we adhere to the Ubuntu code of conduct. * **[Get support](https://askubuntu.com/questions/tagged/netplan)**: Ask Ubuntu is a question and answer site for Ubuntu users and developers. * **[Join our online chat](https://web.libera.chat/gamja/?channels=%23netplan)**: Meet us in `#netplan` on IRC Libera.Chat. * **[Report bugs](https://bugs.launchpad.net/netplan/+filebug)**: We want to know about the problems so we can fix them. * **[Contribute code](https://github.com/canonical/netplan)**: The code is open and we are open to accepting changes to it. Thinking about using Netplan? [Get in touch!](https://netplan.io) netplan-1.0/doc/manpage-footer.md000066400000000000000000000004531457004145200170120ustar00rootroot00000000000000# SEE ALSO **`netplan-generate`**(8), **`netplan-apply`**(8), **`netplan-try`**(8), **`netplan-get`**(8), **`netplan-set`**(8), **`netplan-info`**(8), **`netplan-ip`**(8), **`netplan-rebind`**(8), **`netplan-status`**(8), **`netplan-dbus`**(8), **`systemd-networkd`**(8), **`NetworkManager`**(8) netplan-1.0/doc/manpage-header.md000066400000000000000000000006131457004145200167420ustar00rootroot00000000000000--- title: NETPLAN section: 5 author: - Mathieu Trudel-Lapierre () - Martin Pitt () - Lukas Märdian () ... # NAME `netplan` - YAML network configuration abstraction for various backends # SYNOPSIS **netplan** \[*COMMAND*|help\] # COMMANDS See **netplan help** for a list of available commands on this system. # DESCRIPTION netplan-1.0/doc/meson.build000066400000000000000000000024361457004145200157310ustar00rootroot00000000000000if pandoc.found() custom_target( input: ['manpage-header.md', 'structure-id.md', 'netplan-yaml.md', 'manpage-footer.md'], output: 'netplan.5', command: [pandoc, '-s', '-o', '@OUTPUT@', '--from=markdown-smart', '@INPUT@'], install: true, install_dir: join_paths(get_option('mandir'), 'man5')) custom_target( input: 'netplan-yaml.md', output: 'netplan.html', command: [pandoc, '-s', '--metadata', 'title="Netplan reference"', '--toc', '-o', '@OUTPUT@', '@INPUT@'], install: true, install_dir: join_paths(get_option('datadir'), 'doc', 'netplan')) foreach doc : [ 'netplan-apply', 'netplan-dbus', 'netplan-generate', 'netplan-get', 'netplan-set', 'netplan-try', 'netplan-info', 'netplan-ip', 'netplan-status', 'netplan-rebind', ] markdown = files(doc + '.md') manpage = doc + '.8' custom_target( input: markdown, output: manpage, command: [pandoc, '-s', '-o', '@OUTPUT@', '--shift-heading-level-by=-1', '--from=markdown-smart', '@INPUT@'], install: true, install_dir: join_paths(get_option('mandir'), 'man8')) endforeach else warning('Program "pandoc" not found! Cannot generate documentation/man pages') endif netplan-1.0/doc/netplan-apply.md000066400000000000000000000041221457004145200166670ustar00rootroot00000000000000--- title: NETPLAN-APPLY section: 8 author: - Daniel Axtens () ... ## NAME `netplan-apply` - apply configuration from Netplan YAML files to a running system ## SYNOPSIS **`netplan`** \[*--debug*\] **apply** **-h**|**--help** **`netplan`** \[*--debug*\] **apply** ## DESCRIPTION **`netplan apply`** applies the current Netplan configuration to a running system. The process works as follows: 1. The back-end configuration is generated from Netplan YAML files. 2. The appropriate back ends (**`systemd-networkd`**(8) or **`NetworkManager`**(8)) are invoked to bring up configured interfaces. 3. **`netplan apply`** iterates through interfaces that are still down, unbinding them from their drivers and rebinding them. This gives **`udev`**(7) renaming rules the opportunity to run. 4. If any devices have been rebound, the appropriate back ends are re-invoked in case more matches can be done. For information about the generation step, see **`netplan-generate`**(8). For details of the configuration file format, see **`netplan`**(5). ## OPTIONS `-h`, `--help` : Print basic help. `--debug` : Print debugging output during the process. ## KNOWN ISSUES **`netplan apply`** will not remove virtual devices such as bridges and bonds that have been created, even if they are no longer described in the Netplan configuration. That is due to the fact that Netplan operates statelessly and is not aware of the previously defined virtual devices. This can be resolved by manually removing the virtual device (for example `ip link delete dev bond0`) and then running `netplan apply`, by rebooting, or by creating a temporary backup of the YAML state in `/etc/netplan` before modifying the configuration and passing this state to Netplan (e.g. `mkdir -p /tmp/netplan_state_backup/etc && cp -r /etc/netplan /tmp/netplan_state_backup/etc/` then running `netplan apply --state /tmp/netplan_state_backup`). ## SEE ALSO **`netplan`**(5), **`netplan-generate`**(8), **`netplan-try`**(8), **`udev`**(7), **`systemd-networkd.service`**(8), **`NetworkManager`**(8) netplan-1.0/doc/netplan-dbus.md000066400000000000000000000053361457004145200165070ustar00rootroot00000000000000--- title: NETPLAN-DBUS section: 8 author: - Lukas Märdian () ... ## NAME `netplan-dbus` - daemon to access Netplan functionality via a D-Bus API ## SYNOPSIS **`netplan-dbus`** ## DESCRIPTION **`netplan-dbus`** is a D-Bus daemon, providing `io.netplan.Netplan` on the system bus. The `/io/netplan/Netplan` object provides an `io.netplan.Netplan` interface, offering the following methods: * `Apply() -> b`: calls `netplan apply` and returns a success or failure status. * `Generate() -> b`: calls `netplan generate` and returns a success or failure status. * `Info() -> a(sv)`: returns a dictionary "Features -> as", containing an array of all available feature flags. * `Config() -> o`: prepares a new configuration object as `/io/netplan/Netplan/config/`, by copying the current state from `/{etc,run,lib}/netplan/*.yaml`. The `/io/netplan/Netplan/config/` objects provide a `io.netplan.Netplan.Config` interface, offering the following methods: * `Get() -> s`: calls `netplan get --root-dir=/run/netplan/config-ID all` and returns the merged YAML configuration of the the given configuration object's state * `Set(s:CONFIG_DELTA, s:ORIGIN_HINT) -> b`: calls `netplan set --root-dir=/run/netplan/config-ID --origin-hint=ORIGIN_HINT CONFIG_DELTA` `CONFIG_DELTA` can be something like: `network.ethernets.eth0.dhcp4=true` and `ORIGIN_HINT` can be something like: `70-snapd` (it will then write the configuration to `70-snapd.yaml`). Once `Set()` is called on a configuration object, all other current and future configuration objects are being invalidated and cannot `Set()` or `Try()/Apply()` anymore, due to this pending dirty state. After the dirty configuration object is rejected via `Cancel()`, the other configuration objects are valid again. If the dirty configuration object is accepted via `Apply()`, newly created configuration objects will be valid, while the older states will stay invalid. * `Try(u:TIMEOUT_SEC) -> b`: replaces the main Netplan configuration with this configuration object's state and calls `netplan try --timeout=TIMEOUT_SEC`. * `Cancel() -> b`: rejects a currently running `Try()` attempt on this configuration object and/or discards the configuration object. * `Apply() -> b`: replaces the main Netplan configuration with this configuration object's state and calls `netplan apply`. For information about the `Apply()`/`Try()`/`Get()`/`Set()` functionality, see **`netplan-apply`**(8)/**`netplan-try`**(8)/**`netplan-get`**(8)/**`netplan-set`**(8) accordingly. For details of the configuration file format, see **`netplan`**(5). ## SEE ALSO **`netplan`**(5), **`netplan-apply`**(8), **`netplan-try`**(8), **`netplan-get`**(8), **`netplan-set`**(8) netplan-1.0/doc/netplan-everywhere.md000066400000000000000000000126141457004145200177340ustar00rootroot00000000000000# How to integrate Netplan with desktop ## NetworkManager YAML settings back end NetworkManager is the tool used by Ubuntu Desktop systems to manage network devices such as Ethernet and Wi-Fi adapters. While it is a great tool for the job and users can directly use it through the command line and the graphical interfaces to configure their devices, Ubuntu has its own way of describing and storing network configuration via Netplan. On Ubuntu 23.10 "Mantic Minotaur" and later, NetworkManager uses Netplan APIs to save the configuration created using any of its graphical or programmatic interfaces. This leads to having a centralised location to store network configuration. On the Desktop, it's convenient to use graphical tools for configuration when they are available, so nothing changes from the user perspective; only the way the system handles the configuration in the background. For more information on Netplan, see [netplan.io](https://netplan.io). For more information on NetworkManager, see [networkmanager.dev](https://networkmanager.dev). ## How it works Every time a non-temporary connection is created in NetworkManager, instead of persisting the original `.nmconnection` file, it creates a Netplan YAML file in `/etc/netplan/` called `90-NM-.yaml`. After creating the file, NetworkManager calls the Netplan generator to provide the configuration for that connection. Connections that are temporary, like the ones created for virtual network interfaces when you connect to a VPN for example, are not persisted as Netplan files. The reason for that is that these interfaces are usually managed by external services and we don't want to cause any unexpected change that would affect them. ## How to use ### Installing NetworkManager The NetworkManager 1.44.2 package containing the Netplan integration patch is available by default in Ubuntu 23.10 "Mantic Minotaur" and later as part of the official Ubuntu archive. ``` $ sudo apt update $ sudo apt install network-manager ``` ### User interface From this point on, Netplan is aware of all your network configuration and you can query it using its CLI tools, such as `sudo netplan get` or `sudo netplan status`. All while keeping untouched the traditional way of modifying it using NetworkManager (graphical UI, GNOME Quick Settings, `nmcli`, `nmtui`, D-Bus APIs, ...). ### Management of connection profiles The NetworkManager-Netplan integration imports connection profiles from `/etc/NetworkManager/system-connections/` to Netplan during the installation process. It automatically creates a copy of all your connection profiles during the installation of the new network-manager package in `/root/NetworkManager.bak/system-connections/`. The same migration happens in the background whenever you add or modify any connection profile. You can observe this migration on the `apt-get` command line. Watch for logs like the following: ``` Setting up network-manager (1.44.2-1ubuntu1.2) ... Migrating HomeNet (9d087126-ae71-4992-9e0a-18c5ea92a4ed) to /etc/netplan Migrating eduroam (37d643bb-d81d-4186-9402-7b47632c59b1) to /etc/netplan Migrating DebConf (f862be9c-fb06-4c0f-862f-c8e210ca4941) to /etc/netplan ``` For example, if you have a Wi-Fi connection, you will not find the connection profile file at `/etc/NetworkManager/system-connections/` anymore. Instead, the system removes the profile file, and Netplan creates a new YAML file called `90-NM-.yaml` in `/etc/netplan/` and generates a new ephemeral profile in `/run/NetworkManager/system-connections/`. ## Limitation Netplan doesn't yet support all the configuration options available in NetworkManager (or doesn't know how to interpret some of the keywords found in the key file). After creating a new connection you might find a section called `passthrough` in your YAML file, like in the example below: ```yaml network: version: 2 ethernets: NM-0f7a33ac-512e-4c03-b088-4db00fe3292e: renderer: NetworkManager match: name: "enp1s0" nameservers: addresses: - 8.8.8.8 dhcp4: true wakeonlan: true networkmanager: uuid: "0f7a33ac-512e-4c03-b088-4db00fe3292e" name: "Ethernet connection 1" passthrough: ethernet._: "" ipv4.ignore-auto-dns: "true" ipv6.addr-gen-mode: "default" ipv6.method: "disabled" ipv6.ip6-privacy: "-1" proxy._: "" ``` All the configuration under the `passthrough` mapping is added to the `.nmconnection` file as they are. In cases where the connection type is not supported by Netplan, the system uses the `nm-devices` network type. The example below is an OpenVPN client connection, which is not supported by Netplan at the moment. ```yaml network: version: 2 nm-devices: NM-db5f0f67-1f4c-4d59-8ab8-3d278389cf87: renderer: NetworkManager networkmanager: uuid: "db5f0f67-1f4c-4d59-8ab8-3d278389cf87" name: "myvpnconnection" passthrough: connection.type: "vpn" vpn.ca: "path to ca.crt" vpn.cert: "path to client.crt" vpn.cipher: "AES-256-GCM" vpn.connection-type: "tls" vpn.dev: "tun" vpn.key: "path to client.key" vpn.remote: "1.2.3.4:1194" vpn.service-type: "org.freedesktop.NetworkManager.openvpn" ipv4.method: "auto" ipv6.addr-gen-mode: "default" ipv6.method: "auto" proxy._: "" ``` netplan-1.0/doc/netplan-generate.md000066400000000000000000000053331457004145200173410ustar00rootroot00000000000000--- title: NETPLAN-GENERATE section: 8 author: - Daniel Axtens () ... ## NAME `netplan-generate` - generate back-end configuration from Netplan YAML files ## SYNOPSIS **`netplan`** \[*--debug*\] **generate** **-h**|**--help** **`netplan`** \[*--debug*\] **generate** \[*--root-dir ROOT_DIR*\] \[*--mapping MAPPING*\] ## DESCRIPTION **`netplan generate`** converts Netplan YAML into configuration files understood by the back ends (**`systemd-networkd`**(8) or **`NetworkManager`**(8)). It *does not* apply the generated configuration. You will not normally need to run this directly as it is run by **`netplan apply`**, **`netplan try`**, or at boot. Only if executed during the systemd `initializing` phase (i.e. "Early boot, before `basic.target` is reached"), will it attempt to start/apply the newly created service units. **Requires feature: `generate-just-in-time*`* For details of the configuration file format, see **`netplan`**(5). ## OPTIONS `-h`, `--help` : Print basic help. `--debug` : Print debugging output during the process. `--root-dir` *`ROOT_DIR`* : Instead of looking in `/{lib,etc,run}/netplan`, look in `/ROOT_DIR/{lib,etc,run}/netplan`. `--mapping` *`MAPPING`* : Instead of generating output files, parse the configuration files and print some internal information about the device specified in *`MAPPING`*. ## HANDLING MULTIPLE FILES There are 3 locations that **`netplan generate`** considers: * `/lib/netplan/*.yaml` * `/etc/netplan/*.yaml` * `/run/netplan/*.yaml` If there are multiple files with exactly the same name, then only one will be read. A file in `/run/netplan` will shadow - completely replace - a file with the same name in `/etc/netplan`. A file in `/etc/netplan` will itself shadow a file in `/lib/netplan`. Or, in other words, `/run/netplan` is top priority, then `/etc/netplan`, with `/lib/netplan` having the lowest priority. If there are files with different names, then they are considered in lexicographical order - regardless of the directory they are in. Later files add to or override earlier files. For example, `/run/netplan/10-xyz.yaml` would be updated by `/lib/netplan/20-abc.yaml`. If you have two files with the same key/setting, the following rules apply: * If the values are YAML boolean or scalar values (numbers and strings) the old value is overwritten by the new value. * If the values are sequences, the sequences are concatenated - the new values are appended to the old list. * If the values are mappings, Netplan will examine the elements of the mappings in turn using these rules. ## SEE ALSO **`netplan`**(5), **`netplan-apply`**(8), **`netplan-try`**(8), **`systemd-networkd`**(8), **`NetworkManager`**(8) netplan-1.0/doc/netplan-get.md000066400000000000000000000016461457004145200163310ustar00rootroot00000000000000--- title: NETPLAN-GET section: 8 author: - Lukas Märdian (lukas.maerdian@canonical.com) ... ## NAME `netplan-get` - read merged Netplan YAML configuration ## SYNOPSIS **`netplan`** \[*--debug*\] **get** **-h**|**--help** **`netplan`** \[*--debug*\] **get** \[*--root-dir=ROOT_DIR*\] \[*key*\] ## DESCRIPTION **`netplan get key`** reads all YAML files from `/{etc,lib,run}/netplan/*.yaml` and returns a merged view of the current configuration. You can specify `all` as a key (the default) to get the full YAML tree or extract a subtree by specifying a nested key like: `[network.]ethernets.eth0`. For details of the configuration file format, see **`netplan`**(5). ## OPTIONS `-h`, `--help` : Print basic help. `--debug` : Print debugging output during the process. `--root-dir` : Read YAML files from this root instead of `/`. ## SEE ALSO **`netplan`**(5), **`netplan-set`**(8), **`netplan-dbus`**(8) netplan-1.0/doc/netplan-info.md000066400000000000000000000011641457004145200165000ustar00rootroot00000000000000--- title: NETPLAN-INFO section: 8 author: - Danilo Egea Gondolfo (danilo.egea.gondolfo@canonical.com) ... ## NAME `netplan-info` - show available features ## SYNOPSIS **`netplan`** \[*--debug*\] **info** **-h**|**--help** **`netplan`** \[*--debug*\] **info** \[*--json*|*--yaml*\] ## DESCRIPTION **`netplan info`** displays the supported features. ## OPTIONS `-h`, `--help` : Print basic help. `--debug` : Print debugging output during the process. `--json` : Output version and features in JSON format. `--yaml` : Output version and features in YAML format (default). ## SEE ALSO **`netplan`**(5) netplan-1.0/doc/netplan-ip.md000066400000000000000000000014521457004145200161550ustar00rootroot00000000000000--- title: NETPLAN-IP section: 8 author: - Danilo Egea Gondolfo (danilo.egea.gondolfo@canonical.com) ... ## NAME `netplan-ip` - retrieve IP information (like DHCP leases) from the system ## SYNOPSIS **`netplan`** \[*--debug*\] **ip** **-h**|**--help** **`netplan`** \[*--debug*\] **ip** *COMMAND* \[*--root-dir=ROOT_DIR*\] *ARGUMENTS* ## DESCRIPTION **`netplan ip`** retrieves IP information (like DHCP leases) from the system. ## DHCP COMMANDS **`leases`** `INTERFACE` : Displays DHCP IP leases Example: `netplan ip leases enp5s0` ## OPTIONS `-h`, `--help` : Print basic help. `--debug` : Print debugging output during the process. `--root-dir` : Read YAML files from this root instead of `/`. ## SEE ALSO **`netplan`**(5), **`netplan-get`**(8), **`netplan-status`**(8) netplan-1.0/doc/netplan-rebind.md000066400000000000000000000013101457004145200170010ustar00rootroot00000000000000--- title: NETPLAN-REBIND section: 8 author: - Danilo Egea Gondolfo (danilo.egea.gondolfo@canonical.com) ... ## NAME `netplan-rebind` - rebind SR-IOV virtual functions to their driver ## SYNOPSIS **`netplan`** \[*--debug*\] **rebind** **-h**|**--help** **`netplan`** \[*--debug*\] **rebind** \[*interfaces*\] ## DESCRIPTION **`netplan rebind [interfaces]`** rebinds SR-IOV virtual functions of given physical functions to their driver. ## OPTIONS `-h`, `--help` : Print basic help. `--debug` : Print debugging output during the process. `interfaces` : Space-separated list of physical-function interface names. ## SEE ALSO **`netplan`**(5), **`netplan-set`**(8), **`netplan-apply`**(8) netplan-1.0/doc/netplan-set.md000066400000000000000000000021621457004145200163370ustar00rootroot00000000000000--- title: NETPLAN-SET section: 8 author: - Lukas Märdian (lukas.maerdian@canonical.com) ... ## NAME `netplan-set` - write Netplan YAML configuration snippets to file ## SYNOPSIS **`netplan`** \[*--debug*\] **set** **-h**|**--help** **`netplan`** \[*--debug*\] **set** \[*--root-dir=ROOT_DIR*\] \[*--origin-hint=ORIGIN_HINT*\] \[*key=value*\] ## DESCRIPTION **`netplan set [key=value]`** writes a given key/value pair or YAML subtree into a YAML file in `/etc/netplan/` and validates its format. You can specify a single value as: `"[network.]ethernets.eth0.addresses=[1.2.3.4/24, 5.6.7.8/24]"` or a full subtree as: `"[network.]ethernets.eth0={dhcp4: true, dhcp6: true}"`. For details of the configuration file format, see **`netplan`**(5). ## OPTIONS `-h`, `--help` : Print basic help. `--debug` : Print debugging output during the process. `--root-dir` : Write YAML files into this root instead of `/`. `--origin-hint` : Specify a name for the configuration file, e.g.: `70-netplan-set` => `/etc/netplan/70-netplan-set.yaml`. ## SEE ALSO **`netplan`**(5), **`netplan-get`**(8), **`netplan-dbus`**(8) netplan-1.0/doc/netplan-status.md000066400000000000000000000035461457004145200170760ustar00rootroot00000000000000--- title: NETPLAN-STATUS section: 8 author: - Danilo Egea Gondolfo (danilo.egea.gondolfo@canonical.com) ... ## NAME `netplan-status` - query networking state of the running system ## SYNOPSIS **`netplan`** \[*--debug*\] **status** **-h**|**--help** **`netplan`** \[*--debug*\] **status** \[*interface*\] ## DESCRIPTION **`netplan status [interface]`** queries the current network configuration and displays it in human-readable format. You can specify `interface` to display the status of a specific interface. Currently, **`netplan status`** depends on `systemd-networkd` as a source of data and will try to start it if it's not masked. ## OPTIONS `-h`, `--help` : Print basic help. `--debug` : Print debugging output during the process. `-a`, `--all` : Show all interface data including inactive. `--diff` : Analyze and display differences between the current system configuration and network definitions present in the YAML files. The configuration analyzed includes IP addresses, routes, MAC addresses, DNS addresses, search domains and missing network interfaces. The output format is similar to popular diff tools, such `diff` and `git diff`. Configuration present only in the system (and therefore missing in the Netplan YAMLs) will be displayed with a `+` sign and will be highlighted in green. Configuration present only in Netplan (and therefore missing in the system) will be displayed with a `-` sign and highlighted in red. The same is applied to network interfaces. `--diff-only` : Same as `--diff` but omits all the information that is not a difference. `--root-dir` : Read YAML files from this root instead of `/`. `--verbose` : Show extra information. `-f` *`FORMAT`*, `--format` *`FORMAT`* : Output in machine-readable `json` or `yaml` format. ## SEE ALSO **`netplan`**(5), **`netplan-get`**(8), **`netplan-ip`**(8) netplan-1.0/doc/netplan-try.md000066400000000000000000000034121457004145200163610ustar00rootroot00000000000000--- title: NETPLAN-TRY section: 8 author: - Daniel Axtens () ... ## NAME `netplan-try` - try a configuration, optionally rolling it back ## SYNOPSIS **`netplan`** \[*--debug*\] **try** **-h**|**--help** **`netplan`** \[*--debug*\] **try** \[*--config-file CONFIG_FILE*\] \[*--timeout TIMEOUT*\] ## DESCRIPTION **`netplan try`** takes a **`netplan`**(5) configuration, applies it, and automatically rolls it back if the user does not confirm the configuration within a time limit. A configuration can be confirmed or rejected interactively or by sending the SIGUSR1 or SIGINT signals. This may be especially useful on remote systems, to prevent an administrator being permanently locked out of systems in the case of a network configuration error. ## OPTIONS `-h`, `--help` : Print basic help. `--debug` : Print debugging output during the process. `--config-file` *`CONFIG_FILE`* : In addition to the usual configuration, apply *`CONFIG_FILE`*. It must be a YAML file in the **`netplan`**(5) format. `--timeout` *`TIMEOUT`* : Wait for *`TIMEOUT`* seconds before reverting. Defaults to 120 seconds. Note that some network configurations (such as STP) may take over a minute to settle. ## KNOWN ISSUES **`netplan try`** uses similar procedures to **`netplan apply`**, so some of the same caveats apply around virtual devices. There are also some known bugs: if **`netplan try`** times out or is cancelled, make sure to verify if the network configuration has in fact been reverted. As with **`netplan apply`**, a reboot should fix any issues. However, be sure to verify that the configuration on disk is in the state you expect before rebooting! ## SEE ALSO **`netplan`**(5), **`netplan-generate`**(8), **`netplan-apply`**(8) netplan-1.0/doc/netplan-tutorial.md000066400000000000000000000504251457004145200174140ustar00rootroot00000000000000# Pre-requisites In order to do the exercises yourself you will need a virtual machine, preferably running Ubuntu. In this tutorial, we will use LXD to create virtual networks and launch virtual machines. Feel free to use a cloud instance or a different hypervisor. As long as you can achieve the same results, you should be fine. If you're going to use your own desktop/laptop system, some of the exercises might interrupt your network connectivity. If you already have a setup where you can do the exercises you can just skip this section. ## Setting up the environment You can follow the steps below to install and create a basic LXD configuration you can use to launch virtual machines. For more information about LXD, please visit [linuxcontainers.org](https://documentation.ubuntu.com/lxd/). First, install LXD: [LXD | How to install LXD](https://documentation.ubuntu.com/lxd/en/latest/installing/) On Ubuntu, you can install it using `snap`: ``` snap install lxd ``` Now, initialise your LXD configuration: ``` lxd init --minimal ``` Run the command below to create a new network in LXD. For some of the exercises you will need a second network interface in your virtual machine. ``` lxc network create netplanbr0 --type=bridge ``` You should see the output below: ``` Network netplanbr0 created ``` At this point you should have a usable LXD installation with a working network bridge. Now create a virtual machine called `netplan-lab0`: ``` lxc init --vm ubuntu:22.04 netplan-lab0 ``` You should see the output below: ``` Creating netplan-lab0 ``` The new VM will have one network interface attached to the default LXD bridge. Now attach the network you created just now, `netplanbr0`, to your VM, `netplan-lab0`, as interface `eth1`: ``` lxc network attach netplanbr0 netplan-lab0 eth1 ``` > See more: [LXD | Attach a network to an instance](https://documentation.ubuntu.com/lxd/en/latest/howto/network_create/#attach-a-network-to-an-instance) And start your new VM: ``` lxc start netplan-lab0 ``` Access your new VM using `lxc shell`: ``` lxc shell netplan-lab0 ``` > **If this doesn't work for you**: Try instead `lxc exec netplan-lab0 bash` or `lxc console netplan-lab0`. You should now have a root shell inside your VM: ``` root@netplan-lab0:~# ``` Run the command `ip link` to show your network interfaces: ``` ip link ``` You should see an output similar to the below: ``` 1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp5s0: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 link/ether 00:16:3e:13:ae:10 brd ff:ff:ff:ff:ff:ff 3: enp6s0: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff ``` In this case, `enp5s0` is the primary interface connected to the default LXD network and `enp6s0` is the second interface you added connected to your custom network. Now, let's start with a simple exercise. # Running netplan for the first time Start by typing the command `netplan` in your shell: ``` netplan ``` You should see the output below ``` You need to specify a command usage: /usr/sbin/netplan [-h] [--debug] ... Network configuration in YAML options: -h, --help show this help message and exit --debug Enable debug messages Available commands: help Show this help message apply Apply current Netplan config to running system generate Generate back end specific configuration files from /etc/netplan/*.yaml get Get a setting by specifying a nested key like "ethernets.eth0.addresses", or "all" info Show available features ip Retrieve IP information from the system set Add new setting by specifying a dotted key=value pair like ethernets.eth0.dhcp4=true rebind Rebind SR-IOV virtual functions of given physical functions to their driver status Query networking state of the running system try Try to apply a new Netplan config to running system, with automatic rollback ``` As you can see, Netplan has a number of sub commands. Let's explore some of them. # Showing your current Netplan configuration To show your current configuration, run the command `netplan get`. ``` netplan get ``` You should see an output similar to the one below: ```yaml network: version: 2 ethernets: enp5s0: dhcp4: true ``` It shows you have an Ethernet interface called `enp5s0` and it has DHCP enabled for IPv4. # Showing your current network configuration Netplan 0.106 introduced the `netplan status` command. You can use it to show your system's current network configuration. Try that by typing `netplan status --all` in your console: ``` netplan status --all ``` You should see an output similar to the one below: ``` Online state: online DNS Addresses: 127.0.0.53 (stub) DNS Search: lxd ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:13:ae:10 (Red Hat, Inc.) Addresses: 10.86.126.221/24 (dhcp) fd42:bc43:e20e:8cf7:216:3eff:fe13:ae10/64 fe80::216:3eff:fe13:ae10/64 (link) DNS Addresses: 10.86.126.1 fe80::216:3eff:feab:beb9 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.221 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.221 metric 100 (link) 10.86.126.1 from 10.86.126.221 metric 100 (dhcp, link) fd42:bc43:e20e:8cf7::/64 metric 100 (ra) fe80::/64 metric 256 default via fe80::216:3eff:feab:beb9 metric 100 (ra) ● 3: enp6s0 ethernet DOWN (unmanaged) MAC Address: 00:16:3e:0c:97:8a (Red Hat, Inc.) ``` # Checking the file where your configuration is stored The configuration you just listed is stored at `/etc/netplan`. You can see the contents of the file with the command below: ``` cat /etc/netplan/50-cloud-init.yaml ``` You should see an output similar to this: ```yaml # This file is generated from information provided by the datasource. Changes # to it will not persist across an instance reboot. To disable cloud-init's # network configuration capabilities, write a file # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: # network: {config: disabled} network: version: 2 ethernets: enp5s0: dhcp4: true ``` This file was automatically generated by `cloud-init` when the system was initialised. As noted in the comments, changes to this file will not persist. # Enabling your second network interface with DHCP There are basically 2 ways to create or change Netplan configuration: 1) Using the `netplan set` command 2) Editing the YAML files manually Let's see how you can enable your second network interface using both ways. ## Using `netplan set` For simple tasks, you can use `netplan set` to change your configuration. In the example below you are going to create a new YAML file called `second-interface.yaml` containing only the configuration needed to enable our second interfaces. Considering your second network interface is `enp6s0`, run the command below: ``` netplan set --origin-hint second-interface ethernets.enp6s0.dhcp4=true ``` The command line parameter `--origin-hint` sets the name of the file where the configuration will be stored. Now list the files in the directory `/etc/netplan`: ``` ls /etc/netplan ``` You should see the auto generated `cloud-init` file and a new file called `second-interface.yaml`: ``` root@netplan-lab0:~# ls /etc/netplan/ 50-cloud-init.yaml second-interface.yaml ``` Use the command `cat` to see its content: ``` cat /etc/netplan/second-interface.yaml ``` ```yaml network: version: 2 ethernets: enp6s0: dhcp4: true ``` You will notice it is very similar to the one generated by `cloud-init`. Now use `netplan get` to see your full configuration: ``` netplan get ``` You should see an output similar to the one below with both Ethernet interfaces: ```yaml network: version: 2 ethernets: enp5s0: dhcp4: true enp6s0: dhcp4: true ``` ## Applying your new configuration The command `netplan set` created the configuration for your second network interface but it wasn't applied to the running system. Run the command below to see the current state of your second network interface: ``` ip address show enp6s0 ``` You should see an output similar to the one below: ``` 3: enp6s0: mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff ``` As you can see, this interface has no IP address and its state is DOWN. In order to apply the Netplan configuration, you can use the command `netplan apply`. Run the command below in your shell: ``` netplan apply ``` Now check again the state of the interface `enp6s0`: ``` ip address show enp6s0 ``` You should see an output similar to this: ``` 3: enp6s0: mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff inet 10.33.59.157/24 metric 100 brd 10.33.59.255 scope global dynamic enp6s0 valid_lft 3589sec preferred_lft 3589sec inet6 fd42:ee65:61d0:abcb:216:3eff:fe0c:978a/64 scope global dynamic mngtmpaddr noprefixroute valid_lft 3599sec preferred_lft 3599sec inet6 fe80::216:3eff:fe0c:978a/64 scope link valid_lft forever preferred_lft forever ``` You can also use `netplan status` to check the interface: ``` netplan status enp6s0 ``` You should see an output similar to this: ``` Online state: online DNS Addresses: 127.0.0.53 (stub) DNS Search: lxd ● 3: enp6s0 ethernet UP (networkd: enp6s0) MAC Address: 00:16:3e:0c:97:8a (Red Hat, Inc.) Addresses: 10.33.59.157/24 (dhcp) fd42:ee65:61d0:abcb:216:3eff:fe0c:978a/64 fe80::216:3eff:fe0c:978a/64 (link) DNS Addresses: 10.33.59.1 fe80::216:3eff:fea1:585b DNS Search: lxd Routes: default via 10.33.59.1 from 10.33.59.157 metric 100 (dhcp) 10.33.59.0/24 from 10.33.59.157 metric 100 (link) 10.33.59.1 from 10.33.59.157 metric 100 (dhcp, link) fd42:ee65:61d0:abcb::/64 metric 100 (ra) fe80::/64 metric 256 2 inactive interfaces hidden. Use "--all" to show all. ``` As you can see, even though you haven't enabled DHCP for IPv6 on this interface, the network configuration back end (in this case systemd-networkd) enabled it anyway. But let's assume you want only IPv4. Let's address this situation in the next exercise. ## Editing YAML files For more complex configuration, you can just create or edit a new file yourself using your favourite text editor. Continuing the exercise from the previous section, let's go ahead and disable automatic IPv6 configuration on your second interface. But this time let's do it by manually editing the YAML file. Use your favourite text editor and open the file `/etc/netplan/second-interface.yaml`. Add the configuration below to the interface configuration section: ```yaml accept-ra: false link-local: [] ``` When you finish, it should look like this: ```yaml network: version: 2 ethernets: enp6s0: dhcp4: true accept-ra: false link-local: [] ``` With this new configuration, the network configuration back end (systemd-networkd in this case) will not accept Route Advertisements and will not add the link-local address to our interface. Now check your new configuration with the `netplan get` command: ``` netplan get ``` You should see something similar to this: ```yaml network: version: 2 ethernets: enp5s0: dhcp4: true enp6s0: dhcp4: true accept-ra: false link-local: [] ``` Now use `netplan apply` to apply your new configuration: ``` netplan apply ``` And check your interface configuration: ``` ip address show enp6s0 ``` You should see an output similar to this: ``` 3: enp6s0: mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff inet 10.33.59.157/24 metric 100 brd 10.33.59.255 scope global dynamic enp6s0 valid_lft 3585sec preferred_lft 3585sec ``` And as you can see, now it only has an IPv4 address. In this exercise you explored the `netplan set`, `netplan get`, `netplan apply` and `netplan status` commands. You also used some of the Ethernet configuration options to get a network interface up and running with DHCP. # Using static IP addresses In this exercise you're going to add an static IP address to the second interface with a default route and DNS configuration. Edit the file `/etc/netplan/second-interface.yaml` created previously. Change it so it will look like this: ```yaml network: version: 2 ethernets: enp6s0: dhcp4: false dhcp6: false accept-ra: false link-local: [] addresses: - 172.16.0.1/24 routes: - to: default via: 172.16.0.254 nameservers: search: - netplanlab.local addresses: - 172.16.0.254 - 172.16.0.253 ``` The configuration above is what you'd expect in a desktop system for example. It defines the interface's IP address statically as `172.16.0.1/24`, a default route via gateway `172.16.0.254` and the DNS search domain and name servers. Now use `netplan get` to visualise all your network configuration: ``` netplan get ``` You should see an output similar to this: ```yaml network: version: 2 ethernets: enp5s0: dhcp4: true enp6s0: addresses: - "172.16.0.1/24" nameservers: addresses: - 172.16.0.254 - 172.16.0.253 search: - netplanlab.local dhcp4: false dhcp6: false accept-ra: false routes: - to: "default" via: "172.16.0.254" link-local: [] ``` You will notice that it might be a little different than what you have defined in the YAML file. Some things might be in a different order for example. The reason for that is that `netplan get` loads and parses your configuration before outputting it, and the YAML parsing engine used by Netplan might shuffle things around. Although, what you see from `netplan get` is equivalent to what you have in the file. Now use `netplan apply` to apply the new configuration: ``` netplan apply ``` And check the interface's new state: ``` ip address show dev enp6s0 ``` You should see something similar to this: ``` 3: enp6s0: mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff inet 172.16.0.1/24 brd 172.16.0.255 scope global enp6s0 valid_lft forever preferred_lft forever ``` Check the routes associated to the interface: ``` ip route show dev enp6s0 ``` You should see something similar to this: ``` default via 172.16.0.254 proto static 172.16.0.0/24 proto kernel scope link src 172.16.0.1 ``` And check the DNS configuration: ``` netplan status enp6s0 ``` You should see something similar to this: ``` Online state: online DNS Addresses: 127.0.0.53 (stub) DNS Search: netplanlab.local lxd ● 3: enp6s0 ethernet UP (networkd: enp6s0) MAC Address: 00:16:3e:0c:97:8a (Red Hat, Inc.) Addresses: 172.16.0.1/24 DNS Addresses: 172.16.0.254 172.16.0.253 DNS Search: netplanlab.local Routes: default via 172.16.0.254 (static) 172.16.0.0/24 from 172.16.0.1 (link) 2 inactive interfaces hidden. Use "--all" to show all. ``` # Matching the interface by MAC address Sometimes you can't rely on the interface names to apply configuration to them. Changes in the system might cause a change in their names, such as when you move an interface card from a PCI slot to another. In this exercise you will use the `match` keyword to locate the device based on its MAC address and also set a more meaningful name to the interface. Let's assume that your second interface is connected to the Netplan ISP internet provider company and you want to identify it as such. First identify its MAC address: ``` ip link show enp6s0 ``` In the output, the MAC address is the number in front of the `link/ether` property. ``` 3: enp6s0: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff ``` Edit the file `/etc/netplan/second-interface.yaml` and make the following changes: ```yaml network: version: 2 ethernets: netplan-isp-interface: match: macaddress: 00:16:3e:0c:97:8a set-name: netplan-isp dhcp4: false dhcp6: false accept-ra: false link-local: [] addresses: - 172.16.0.1/24 routes: - to: default via: 172.16.0.254 nameservers: search: - netplanlab.local addresses: - 172.16.0.254 - 172.16.0.253 ``` These are the important changes in this exercise: ```yaml ethernets: netplan-isp-interface: match: macaddress: 00:16:3e:0c:97:8a set-name: netplan-isp ``` Note that, as you are now matching the interface by its MAC address, you are free to identify it with a different name. It makes it easier to read and find information in the YAML file. After changing the file, apply your new configuration: ``` netplan apply ``` Now list your interfaces: ``` ip link show ``` As you can see, your interface is now called `netplan-isp`. ``` 1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp5s0: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 link/ether 00:16:3e:13:ae:10 brd ff:ff:ff:ff:ff:ff 3: netplan-isp: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff ``` # Creating a link aggregation Let's suppose now that you need to configure your system to connect to your ISP links via a link aggregation. On Linux you can do that with a `bond` virtual interface. On Netplan, an interface of type `bond` can be created inside a `bonds` mapping. Now that the traffic will flow through the link aggregation, you will move all the addressing configuration to the bond itself. You can define a list of interfaces that will be attached to the bond. In our simple scenario, we have a single one. Edit the file `/etc/netplan/second-interface.yaml` and make the following changes: ```yaml network: version: 2 ethernets: netplan-isp-interface: dhcp4: false dhcp6: false match: macaddress: 00:16:3e:0c:97:8a set-name: netplan-isp bonds: isp-bond0: interfaces: - netplan-isp-interface dhcp4: false dhcp6: false accept-ra: false link-local: [] addresses: - 172.16.0.1/24 routes: - to: default via: 172.16.0.254 nameservers: search: - netplanlab.local addresses: - 172.16.0.254 - 172.16.0.253 ``` Note that you can reference the interface used in the bond by the name you defined for it in the `ethernets` section. Now use `netplan apply` to apply your changes ``` netplan apply ``` Now your system has a new interface called `isp-bond0`. Use the command `ip address show isp-bond0` or `netplan status` to check its state: ``` netplan status isp-bond0 ``` You should see an output similar to the one below: ``` Online state: online DNS Addresses: 127.0.0.53 (stub) DNS Search: lxd netplanlab.local ● 4: isp-bond0 bond UP (networkd: isp-bond0) MAC Address: b2:6b:19:b1:9a:86 Addresses: 172.16.0.1/24 DNS Addresses: 172.16.0.254 172.16.0.253 DNS Search: netplanlab.local Routes: default via 172.16.0.254 (static) 172.16.0.0/24 from 172.16.0.1 (link) 3 inactive interfaces hidden. Use "--all" to show all. ``` netplan-1.0/doc/netplan-yaml.md000066400000000000000000002131141457004145200165070ustar00rootroot00000000000000# YAML configuration ## Top-level configuration structure The general structure of a Netplan YAML file is shown below. ```yaml network: version: NUMBER renderer: STRING bonds: MAPPING bridges: MAPPING dummy-devices: MAPPING ethernets: MAPPING modems: MAPPING tunnels: MAPPING virtual-ethernets: MAPPING vlans: MAPPING vrfs: MAPPING wifis: MAPPING nm-devices: MAPPING ``` - **`version`** (number) > Defines what version of the configuration format is used. The only value supported is `2`. Defaults to `2` if not defined. - **`renderer`** (scalar) > Defines what network configuration tool will be used to set up your configuration. Valid values are networkd and `NetworkManager`. Defaults to networkd if not defined. - [**`bonds`**](#properties-for-device-type-bonds) (mapping) > Creates and configures link aggregation (bonding) devices. - [**`bridges`**](#properties-for-device-type-bridges) (mapping) > Creates and configures bridge devices. - [**`dummy-devices`**](#properties-for-device-type-dummy-devices) (mapping) – since 0.107 > Creates and configures virtual devices. - [**`ethernets`**](#properties-for-device-type-ethernets) (mapping) > Configures physical Ethernet interfaces. - [**`modems`**](#properties-for-device-type-modems) (mapping) > Configures modems - [**`tunnels`**](#properties-for-device-type-tunnels) (mapping) > Creates and configures different types of virtual tunnels. - [**`virtual-ethernets`**](#properties-for-device-type-virtual-ethernets) (mapping) – since 0.107 > Creates and configures Virtual Ethernet (`veth`) devices. - [**`vlans`**](#properties-for-device-type-vlans) (mapping) > Creates and configures VLANs. - [**`vrfs`**](#properties-for-device-type-vrfs) (mapping) > Configures Virtual Routing and Forwarding (VRF) devices. - [**`wifis`**](#properties-for-device-type-wifis) (mapping) > Configures physical Wi-Fi interfaces as `client`, `adhoc` or `access point`. - [**`nm-devices`**](#properties-for-device-type-nm-devices) (mapping) > `nm-devices` are used in situations where Netplan doesn't support the connection type. The raw configuration expected by NetworkManager can be defined and will be passed as is (`passthrough`) to the `.nmconnection` file. Users will not normally use this type of device. All the properties for all the device types will be described in the next sections. ## Properties for physical device types These properties are used with physical devices such as Ethernet and Wi-Fi network interfaces. **Note:** Some options will not work reliably for devices matched by name only and rendered by networkd, due to interactions with device renaming in udev. Match devices by MAC when setting options like: `wakeonlan` or `*-offload`. - **`match`** (mapping) > This selects a subset of available physical devices by various hardware > properties. The following configuration will then apply to all matching > devices, as soon as they appear. *All* specified properties must match. - **`name`** (scalar) > Current interface name. Globs are supported, and the primary use case for > matching on names, as selecting one fixed name can be more easily achieved > with having no `match:` at all and just using the ID (see above). > (`NetworkManager`: as of v1.14.0) - **`macaddress`** (scalar) > 6-byte permanent MAC address of the device in the form `XX:XX:XX:XX:XX:XX` or > 20 bytes for InfiniBand devices (IPoIB). Globs are not allowed. > This doesn't match virtual MAC addresses for `veth`, `bridge`, `bond`, `vlan`, ... - **`driver`** (scalar or sequence of scalars) – sequence since 0.104 > Kernel driver name, corresponding to the `DRIVER` udev property. > A sequence of globs is supported, any of which must match. > Matching on driver is *only* supported with networkd. Examples: - All cards on second PCI bus: ```yaml network: ethernets: myinterface: match: name: enp2* ``` - Fixed MAC address: ```yaml network: ethernets: interface0: match: macaddress: 11:22:33:AA:BB:FF ``` - First card of driver `ixgbe`: ```yaml network: ethernets: nic0: match: driver: ixgbe name: en*s0 ``` - First card with a driver matching `bcmgenet` or `smsc*`: ```yaml network: ethernets: nic0: match: driver: ["bcmgenet", "smsc*"] name: en* ``` - **`set-name`** (scalar) > When matching on unique properties such as path or MAC, or with additional > assumptions such as "there will only ever be one Wi-Fi device", match rules > can be written so that they only match one device. Then this property can be > used to give that device a more specific or desirable name than the > default from udev ifnames. Any additional device that satisfies the match > rules will then fail to get renamed and keep the original kernel name (and > `dmesg` will show an error). - **`wakeonlan`** (boolean) > Enable wake on LAN. Off by default. - **`emit-lldp`** (boolean) – since 0.99 > (networkd back end only) Whether to emit LLDP packets. Off by default. - **`receive-checksum-offload`** (boolean) – since 0.104 > (networkd back end only) If set to `true` (`false`), the hardware offload for > checksumming of ingress network packets is enabled (disabled). When unset, > the kernel's default will be used. - **`transmit-checksum-offload`** (boolean) – since 0.104 > (networkd back end only) If set to `true` (`false`), the hardware offload for > checksumming of egress network packets is enabled (disabled). When unset, > the kernel's default will be used. - **`tcp-segmentation-offload`** (boolean) – since 0.104 > (networkd back end only) If set to `true` (`false`), the TCP Segmentation > Offload (TSO) is enabled (disabled). When unset, the kernel's default will > be used. - **`tcp6-segmentation-offload`** (boolean) – since 0.104 > (networkd back end only) If set to `true` (`false`), the TCP6 Segmentation > Offload (`tx-tcp6-segmentation`) is enabled (disabled). When unset, the > kernel's default will be used. - **`generic-segmentation-offload`** (boolean) – since 0.104 > (networkd back end only) If set to `true` (`false`), the Generic Segmentation > Offload (GSO) is enabled (disabled). When unset, the kernel's default will > be used. - **`generic-receive-offload`** (boolean) – since 0.104 > (networkd back end only) If set to `true` (`false`), the Generic Receive > Offload (GRO) is enabled (disabled). When unset, the kernel's default will > be used. - **`large-receive-offload`** (boolean) – since 0.104 > (networkd back end only) If set to `true` (`false`), the Large Receive Offload > (LRO) is enabled (disabled). When unset, the kernel's default will > be used. - **`openvswitch`** (mapping) – since 0.100 > This provides additional configuration for the `openvswitch` network device. > If Open vSwitch is not available on the system, Netplan treats the presence > of `openvswitch` configuration as an error. > > Any supported network device that is declared with the `openvswitch` > mapping (or any bond/bridge that includes an interface with an `openvswitch` > configuration) will be created in `openvswitch` instead of the defined > renderer. In the case of a `vlan` definition declared the same way, > Netplan will create a fake VLAN bridge in `openvswitch` with the requested > `vlan` properties. - **`external-ids`** (mapping) – since 0.100 > Passed-through directly to Open vSwitch - **`other-config`** (mapping) – since 0.100 > Passed-through directly to Open vSwitch - **`lacp`** (scalar) – since 0.100 > Valid for bond interfaces. Accepts `active`, `passive` or `off` (the > default). - **`fail-mode`** (scalar) – since 0.100 > Valid for bridge interfaces. Accepts `secure` or `standalone` (the > default). - **`mcast-snooping`** (boolean) – since 0.100 > Valid for bridge interfaces. False by default. - **`protocols`** (sequence of scalars) – since 0.100 > Valid for bridge interfaces or the network section. List of protocols to > be used when negotiating a connection with the controller. Accepts > `OpenFlow10`, `OpenFlow11`, `OpenFlow12`, `OpenFlow13`, `OpenFlow14`, > and `OpenFlow15`. - **`rstp`** (boolean) – since 0.100 > Valid for bridge interfaces. False by default. - **`controller`** (mapping) – since 0.100 > Valid for bridge interfaces. Specify an external OpenFlow controller. - **`addresses`** (sequence of scalars) > Set the list of addresses to use for the controller targets. The > syntax of these addresses is as defined in **`ovs-vsctl`**(8). Example: > addresses: `[tcp:127.0.0.1:6653, "ssl:[fe80::1234%eth0]:6653"]`. - **`connection-mode`** (scalar) > Set the connection mode for the controller. Supported options are > `in-band` and `out-of-band`. The default is `in-band`. - **`ports`** (sequence of sequence of scalars) – since 0.100 > Open vSwitch patch ports. Each port is declared as a pair of names > which can be referenced as interfaces in dependent virtual devices > (bonds, bridges). Example: ```yaml openvswitch: ports: - [patch0-1, patch1-0] ``` - **`ssl`** (mapping) – since 0.100 > Valid for global `openvswitch` settings. Options for configuring SSL > server endpoint for the switch. - **`ca-cert`** (scalar) > Path to a file containing the CA certificate to be used. - **`certificate`** (scalar) > Path to a file containing the server certificate. - **`private-key`** (scalar) > Path to a file containing the private key for the server. ## Properties for all device types - **`renderer`** (scalar) > Use the given networking back end for this definition. Currently supported > are `networkd` and `NetworkManager`. This property can be specified globally > in `network:`, for a device type (in e.g. `ethernets:`) or > for a particular device definition. Default is `networkd`. > > (Since 0.99) The `renderer` property has one additional acceptable value for > VLAN objects (i.e. defined in `vlans:`): `sriov`. If a VLAN is defined with > the `sriov` renderer for an SR-IOV Virtual Function interface, this causes > Netplan to set up a hardware VLAN filter for it. There can be only one > defined per VF. - **`dhcp4`** (boolean) > Enable DHCP for IPv4. Off by default. - **`dhcp6`** (boolean) > Enable DHCP for IPv6. Off by default. This covers both stateless DHCP - > where the DHCP server supplies information like DNS name servers but not the > IP address - and stateful DHCP, where the server provides both the address > and the other information. > > If you are in an IPv6-only environment with completely stateless > auto-configuration (SLAAC with RDNSS), this option can be set to cause the > interface to be brought up. (Setting `accept-ra` alone is not sufficient.) > Auto-configuration will still honour the contents of the router > advertisement and only use DHCP if requested in the RA. > > Note that **`rdnssd`**(8) is required to use RDNSS with networkd. No extra > software is required for NetworkManager. - **`ipv6-mtu`** (scalar) – since 0.98 > Set the IPv6 MTU (only supported with networkd back end). Note > that needing to set this is an unusual requirement. > > **Requires feature: `ipv6-mtu`** - **`ipv6-privacy`** (boolean) > Enable IPv6 Privacy Extensions (RFC 4941) for the specified interface, and > prefer temporary addresses. Defaults to false - no privacy extensions. There > is currently no way to have a private address but prefer the public address. - **`link-local`** (sequence of scalars) > Configure the link-local addresses to bring up. Valid options are `ipv4` > and `ipv6`, which respectively allow enabling IPv4 and IPv6 link local > addressing. If this field is not defined, the default is to enable only > IPv6 link-local addresses. If the field is defined but configured as an > empty set, IPv6 link-local addresses are disabled as well as IPv4 link- > local addresses. > > This feature enables or disables link-local addresses for a protocol, but > the actual implementation differs per back end. On networkd, this directly > changes the behaviour and may add an extra address on an interface. When > using the NetworkManager back end, enabling link-local has no effect if the > interface also has DHCP enabled. Examples: - Enable only IPv4 link-local: `link-local: [ ipv4 ]` - Enable all link-local addresses: `link-local: [ ipv4, ipv6 ]` - Disable all link-local addresses: `link-local: [ ]` - **`ignore-carrier`** (boolean) – since 0.104 > (networkd back end only) Allow the specified interface to be configured even > if it has no carrier. - **`critical`** (boolean) > Designate the connection as "critical to the system", meaning that special > care will be taken by to not release the assigned IP when the daemon is > restarted. (not recognised by NetworkManager) - **`dhcp-identifier`** (scalar) > (networkd back end only) Sets the source of DHCP (v4) client identifier. If > `mac` is specified, the MAC address of the link is used. If this option is > omitted, or if `duid` is specified, networkd will generate an > RFC4361-compliant client identifier for the interface by combining the > link's IAID and DUID. - **`dhcp4-overrides`** (mapping) > (networkd back end only) Overrides default DHCP behaviour; see the > `DHCP Overrides` section below. - **`dhcp6-overrides`** (mapping) > (networkd back end only) Overrides default DHCP behaviour; see the > `DHCP Overrides` section below. - **`accept-ra`** (boolean) > Accept Router Advertisement that would have the kernel configure IPv6 by > itself. When enabled, accept Router Advertisements. When disabled, do not > respond to Router Advertisements. If unset use the host kernel default > setting. - **`addresses`** (sequence of scalars and mappings) > Add static addresses to the interface in addition to the ones received > through DHCP or RA. Each sequence entry is in CIDR notation, i.e. of the > form `addr/prefixlen`. `addr` is an IPv4 or IPv6 address as recognised > by **`inet_pton`**(3) and `prefixlen` the number of bits of the subnet. > > For virtual devices (bridges, bonds, VLAN) if there is no address > configured and DHCP is disabled, the interface may still be brought online, > but will not be addressable from the network. > > In addition to the addresses themselves one can specify configuration > parameters as mappings. Current supported options are: - **`lifetime`** (scalar) – since 0.100 > Default: `forever`. This can be `forever` or `0` and corresponds > to the `PreferredLifetime` option in the `Address` section of `systemd-networkd`. > Currently supported on the networkd back end only. - **`label`** (scalar) – since 0.100 > An IP address label, equivalent to the `ip address label` > command. Currently supported on the networkd back end only. Examples: - Simple: `addresses: [192.168.14.2/24, "2001:1::1/64"]` - Advanced: ```yaml network: ethernets: eth0: addresses: - "10.0.0.15/24": lifetime: 0 label: "maas" - "2001:1::1/64" ``` - **`ipv6-address-generation`** (scalar) – since 0.99 > Configure method for creating the address for use with RFC4862 IPv6 > Stateless Address Auto-configuration (only supported with `NetworkManager` > back end). Possible values are `eui64` or `stable-privacy`. - **`ipv6-address-token`** (scalar) – since 0.100 > Define an IPv6 address token for creating a static interface identifier for > IPv6 Stateless Address Auto-configuration. This is mutually exclusive with > `ipv6-address-generation`. - **`gateway4`**, **`gateway6`** (scalar) > Deprecated, see `Default routes`. > Set default gateway for IPv4/6, for manual address configuration. This > requires setting `addresses` too. Gateway IP addresses must be in a form > recognised by **`inet_pton`**(3). There should only be a single gateway > per IP address family set in your global configuration, to make it unambiguous. > If you need multiple default routes, please define them via > `routing-policy`. Examples - IPv4: `gateway4: 172.16.0.1` - IPv6: `gateway6: "2001:4::1"` - **`nameservers`** (mapping) > Set DNS servers and search domains, for manual address configuration. There > are two supported fields: `addresses:` is a list of IPv4 or IPv6 addresses > similar to `gateway*`, and `search:` is a list of search domains. Example: ```yaml network: ethernets: id0: [...] nameservers: search: [lab, home] addresses: [8.8.8.8, "FEDC::1"] ``` - **`macaddress`** (scalar) > Set the device's MAC address. The MAC address must be in the form > "XX:XX:XX:XX:XX:XX". The following special options are also accepted: > `permanent` and `random`. > In addition to these options, the NetworkManager renderer also accepts > `stable` and `preserve`. > > **Note:** This will not work reliably for devices matched by name > only and rendered by networkd, due to interactions with device > renaming in udev. Match devices by MAC when setting MAC addresses. Example: ```yaml network: ethernets: id0: match: macaddress: 52:54:00:6b:3c:58 [...] macaddress: 52:54:00:6b:3c:59 ``` - **`mtu`** (scalar) > Set the Maximum Transmission Unit for the interface. The default is 1500. > Valid values depend on your network interface. > > **Note:** This will not work reliably for devices matched by name > only and rendered by networkd, due to interactions with device > renaming in udev. Match devices by MAC when setting MTU. - **`optional`** (boolean) > An optional device is not required for booting. Normally, networkd will > wait some time for device to become configured before proceeding with > booting. However, if a device is marked as optional, networkd will not wait > for it. This is *only* supported by networkd, and the default is false. Example: ```yaml network: ethernets: eth7: # this is plugged into a test network that is often # down - don't wait for it to come up during boot. dhcp4: true optional: true ``` - **`optional-addresses`** (sequence of scalars) > Specify types of addresses that are not required for a device to be > considered online. This changes the behaviour of back ends at boot time to > avoid waiting for addresses that are marked optional, and thus consider > the interface as "usable" sooner. This does not disable these addresses, > which will be brought up anyway. Example: ```yaml network: ethernets: eth7: dhcp4: true dhcp6: true optional-addresses: [ ipv4-ll, dhcp6 ] ``` - **`activation-mode`** (scalar) – since 0.103 > Allows specifying the management policy of the selected interface. By > default, Netplan brings up any configured interface if possible. Using the > `activation-mode` setting users can override that behaviour by either > specifying `manual`, to hand over control over the interface state to the > administrator or (for networkd back end *only*) `off` to force the link > in a down state at all times. Any interface with `activation-mode` > defined is implicitly considered `optional`. > Supported officially as of networkd v248+. Example: ```yaml network: ethernets: eth1: # this interface will not be put into an UP state automatically dhcp4: true activation-mode: manual ``` - **`routes`** (sequence of mappings) > Configure static routing for the device; see the `Routing` section below. - **`routing-policy`** (sequence of mappings) > Configure policy routing for the device; see the `Routing` section below. - **`neigh-suppress`** (scalar) – since 0.105 > Takes a boolean. Configures whether ARP and ND neighbour suppression is > enabled for this bridge port. When unset, the kernel's default will be used. - **`hairpin`** (scalar) – since **1.0** > Takes a boolean. Configures whether traffic may be sent back out of the > bridge port on which it was received. When this flag is false, then the > bridge will not forward traffic back out of the receiving port. When > unset, the backend's default will be used. - **`port-mac-learning`** (scalar) – since **1.0** > Takes a boolean. Configures whether MAC address learning is enabled for > this bridge port. When unset, the kernel's default will be used. > Currently supported on the `networkd` backend only. ## DHCP Overrides Several DHCP behaviour overrides are available. Most currently only have any effect when using the networkd back end, with the exception of `use-routes` and `route-metric`. Overrides only have an effect if the corresponding `dhcp4` or `dhcp6` is set to `true`. If both `dhcp4` and `dhcp6` are `true`, the networkd back end requires that `dhcp4-overrides` and `dhcp6-overrides` contain the same keys and values. If the values do not match, an error will be shown and the network configuration will not be applied. When using the NetworkManager back end, different values may be specified for `dhcp4-overrides` and `dhcp6-overrides`, and will be applied to the DHCP client processes as specified in the Netplan YAML. - **`dhcp4-overrides`**, **`dhcp6-overrides`** (mapping) > The `dhcp4-overrides` and `dhcp6-override` mappings override the > default DHCP behaviour. - **`use-dns`** (boolean) > Default: `true`. When `true`, the DNS servers received from the > DHCP server will be used and take precedence over any statically > configured ones. Currently only has an effect on the networkd > back end. - **`use-ntp`** (boolean) > Default: `true`. When `true`, the NTP servers received from the > DHCP server will be used by `systemd-timesyncd` and take precedence > over any statically configured ones. Currently only has an effect on > the networkd back end. - **`send-hostname`** (boolean) > Default: `true`. When `true`, the machine hostname will be sent > to the DHCP server. Currently only has an effect on the networkd > back end. - **`use-hostname`** (boolean) > Default: `true`. When `true`, the hostname received from the DHCP > server will be set as the transient hostname of the system. Currently > only has an effect on the networkd back end. - **`use-mtu`** (boolean) > Default: `true`. When `true`, the MTU received from the DHCP > server will be set as the MTU of the network interface. When `false`, > the MTU advertised by the DHCP server will be ignored. Currently only > has an effect on the networkd back end. - **`hostname`** (scalar) > Use this value for the hostname which is sent to the DHCP server, > instead of machine's hostname. Currently only has an effect on the > networkd back end. - **`use-routes`** (boolean) > Default: `true`. When `true`, the routes received from the DHCP > server will be installed in the routing table normally. When set to > `false`, routes from the DHCP server will be ignored: in this case, > the user is responsible for adding static routes if necessary for > correct network operation. This allows users to avoid installing a > default gateway for interfaces configured via DHCP. Available for > both the networkd and `NetworkManager` back ends. - **`route-metric`** (scalar) > Use this value for default metric for automatically-added routes. > Use this to prioritise routes for devices by setting a lower metric > on a preferred interface. Available for both the networkd and > NetworkManager back ends. - **`use-domains`** (scalar) – since 0.98 > Takes a boolean, or the special value `route`. When true, the domain > name received from the DHCP server will be used as DNS search domain > over this link, similar to the effect of the `Domains=` setting. If set > to `route`, the domain name received from the DHCP server will be > used for routing DNS queries only, but not for searching, similar to > the effect of the `Domains=` setting when the argument is prefixed with > `~` (tilde). > > **Requires feature: `dhcp-use-domains`** ## Routing Complex routing is possible with Netplan. Standard static routes as well as policy routing using routing tables are supported via the networkd back end. These options are available for all types of interfaces. ### Default routes The most common need for routing concerns the definition of default routes to reach the wider internet. Those default routes can only defined once per IP family and routing table. A typical example would look like the following: ```yaml network: ethernets: eth0: [...] routes: - to: default # could be 0.0.0.0/0 optionally via: 10.0.0.1 metric: 100 on-link: true - to: default # could be ::/0 optionally via: cf02:de:ad:be:ef::2 eth1: [...] routes: - to: default via: 172.134.67.1 metric: 100 on-link: true # Not on the main routing table, # does not conflict with the eth0 default route table: 76 ``` - **`routes`** (mapping) > The `routes` block defines standard static routes for an interface. > At least `to` must be specified. If type is `local` or `nat` a > default scope of `host` is assumed. > If type is `unicast` and no gateway (`via`) is given or type is > `broadcast`, `multicast` or `anycast` a default scope of `link` > is assumed. Otherwise, a `global` scope is the default setting. > > For `from`, `to` and `via`, both IPv4 and IPv6 addresses are > recognised, and must be in the form `addr/prefixlen` or `addr`. - **`from`** (scalar) > Set a source IP address for traffic going through the route. > (NetworkManager: as of v1.8.0) - **`to`** (scalar) > Destination address for the route. - **`via`** (scalar) > Address to the gateway to use for this route. - **`on-link`** (boolean) > When set to `true`, specifies that the route is directly connected > to the interface. > (`NetworkManager`: as of v1.12.0 for IPv4 and v1.18.0 for IPv6) - **`metric`** (scalar) > The relative priority of the route. Must be a positive integer value. - **`type`** (scalar) > The type of route. Valid options are `unicast` (default), `anycast`, > `blackhole`, `broadcast`, `local`, `multicast`, `nat`, `prohibit`, > `throw`, `unreachable` or `xresolve`. - **`scope`** (scalar) > The route scope, how wide-ranging it is to the network. Possible > values are `global`, `link`, or `host`. Applies to IPv4 only. - **`table`** (scalar) > The table number to use for the route. In some scenarios, it may be > useful to set routes in a separate routing table. It may also be used > to refer to routing policy rules which also accept a `table` > parameter. Allowed values are positive integers starting from 1. > Some values are already in use to refer to specific routing tables: > see `/etc/iproute2/rt_tables`. > (`NetworkManager`: as of v1.10.0) - **`mtu`** (scalar) – since 0.101 > The MTU to be used for the route, in bytes. Must be a positive integer > value. - **`congestion-window`** (scalar) – since 0.102 > The congestion window to be used for the route, represented by number > of segments. Must be a positive integer value. - **`advertised-receive-window`** (scalar) – since 0.102 > The receive window to be advertised for the route, represented by > number of segments. Must be a positive integer value. - **`routing-policy`** (mapping) > The `routing-policy` block defines extra routing policy for a network, > where traffic may be handled specially based on the source IP, firewall > marking, etc. > > For `from`, `to`, both IPv4 and IPv6 addresses are recognised, and > must be in the form `addr/prefixlen` or `addr`. - **`from`** (scalar) > Set a source IP address to match traffic for this policy rule. - **`to`** (scalar) > Match on traffic going to the specified destination. - **`table`** (scalar) > The table number to match for the route. In some scenarios, it may be > useful to set routes in a separate routing table. It may also be used > to refer to routes which also accept a `table` parameter. > Allowed values are positive integers starting from `1`. > Some values are already in use to refer to specific routing tables: > see `/etc/iproute2/rt_tables`. - **`priority`** (scalar) > Specify a priority for the routing policy rule, to influence the order > in which routing rules are processed. A higher number means lower > priority: rules are processed in order by increasing priority number. - **`mark`** (scalar) > Have this routing policy rule match on traffic that has been marked > by the iptables firewall with this value. Allowed values are positive > integers starting from `1`. - **`type-of-service`** (scalar) > Match this policy rule based on the type of service number applied to > the traffic. ## Authentication Netplan supports advanced authentication settings for Ethernet and Wi-Fi interfaces, as well as individual Wi-Fi networks, by means of the `auth` block. - **`auth`** (mapping) > Specifies authentication settings for a device of type `ethernets:`, or > an `access-points:` entry on a `wifis:` device. > > The `auth` block supports the following properties: - **`key-management`** (scalar) > The supported key management modes are `none` (no key management); > `psk` (WPA with pre-shared key, common for home Wi-Fi); `eap` (WPA > with EAP, common for enterprise Wi-Fi); `eap-sha256` (used with WPA3-Enterprise); > `eap-suite-b-192` (used with WPA3-Enterprise); `sae` (used by WPA3); > and `802.1x` (used primarily for wired Ethernet connections). - **`password`** (scalar) > The password string for EAP, or the pre-shared key for WPA-PSK. The following properties can be used if `key-management` is `eap` or `802.1x`: - **`method`** (scalar) > The EAP method to use. The supported EAP methods are `tls` (TLS), > `peap` (Protected EAP), `leap` (Lightweight EAP), `pwd` (EAP Password) > and `ttls` (Tunnelled TLS). - **`identity`** (scalar) > The identity to use for EAP. - **`anonymous-identity`** (scalar) > The identity to pass over the unencrypted channel if the chosen EAP > method supports passing a different tunnelled identity. - **`ca-certificate`** (scalar) > Path to a file with one or more trusted certificate authority (CA) > certificates. - **`client-certificate`** (scalar) > Path to a file containing the certificate to be used by the client > during authentication. - **`client-key`** (scalar) > Path to a file containing the private key corresponding to > `client-certificate`. - **`client-key-password`** (scalar) > Password to use to decrypt the private key specified in > `client-key` if it is encrypted. - **`phase2-auth`** (scalar) – since 0.99 > Phase 2 authentication mechanism. ## Properties for device type `ethernets` **`Status`**: Optional. **`Purpose`**: Use the `ethernets` key to configure Ethernet interfaces. **`Structure`**: The key consists of a mapping of Ethernet interface IDs. Each `ethernet` has a number of configuration options. You don't need to define each interface by their name inside the `ethernets` mapping. You can use any ID that describes the interface and match the actual network card using the `match` key. The general configuration structure for Ethernet is shown below. ```yaml network: ethernets: device-id: ... ``` `device-id` is the interface identifier. If you use the interface name as the ID, Netplan will match that interface. Consider the example below. In this case, an interface called `eth0` will be configured with DHCP. ```yaml network: ethernets: eth0: dhcp4: true ``` The `device-id` can be any descriptive name your find meaningful. Although, if it doesn't match a real interface name, you must use the property `match` to identify the device you want to configure. The example below defines an Ethernet connection called `isp-interface` (supposedly an external interface connected to the Internet Service Provider) and uses `match` to apply the configuration to the physical device with MAC address `aa:bb:cc:00:11:22`. ```yaml network: ethernets: isp-interface: match: macaddress: aa:bb:cc:00:11:22 dhcp4: true ``` Ethernet device definitions, beyond common ones described above, also support some additional properties that can be used for SR-IOV devices. - **`link`** (scalar) – since 0.99 > (SR-IOV devices only) The `link` property declares the device as a > Virtual Function of the selected Physical Function device, as identified > by the given Netplan ID. Example: ```yaml network: ethernets: enp1: {...} enp1s16f1: link: enp1 ``` - **`virtual-function-count`** (scalar) – since 0.99 > (SR-IOV devices only) In certain special cases VFs might need to be > configured outside of Netplan. For such configurations > `virtual-function-count` can be optionally used to set an explicit number of > Virtual Functions for the given Physical Function. If unset, the default is > to create only as many VFs as are defined in the Netplan configuration. This > should be used for special cases only. > > **Requires feature: `sriov`** - **`embedded-switch-mode`** (scalar) – since 0.104 > (SR-IOV devices only) Change the operational mode of the embedded switch > of a supported SmartNIC PCI device (e.g. Mellanox ConnectX-5). Possible > values are `switchdev` or `legacy`, if unspecified the vendor's > default configuration is used. > > **Requires feature: `eswitch-mode`** - **`delay-virtual-functions-rebind`** (boolean) – since 0.104 > (SR-IOV devices only) Delay rebinding of SR-IOV virtual functions to its > driver after changing the embedded-switch-mode setting to a later stage. > Can be enabled when bonding/VF LAG is in use. Defaults to `false`. > > **Requires feature: `eswitch-mode`** - **`infiniband-mode`** (scalar) – since 0.105 > (InfiniBand devices only) Change the operational mode of a IPoIB device. > Possible values are `datagram` or `connected`. If unspecified the > kernel's default configuration is used. > > **Requires feature: `infiniband`** ## Properties for device type `modems` **Status**: Optional. **Purpose**: Use the `modems` key to configure modem interfaces. GSM/CDMA modem configuration is only supported for the `NetworkManager` back end. `systemd-networkd` does not support modems. **Structure**: The key consists of a mapping of modem IDs. Each `modem` has a number of configuration options. The general configuration structure for Modems is shown below. ```yaml network: version: 2 renderer: NetworkManager modems: cdc-wdm1: mtu: 1600 apn: ISP.CINGULAR username: ISP@CINGULARGPRS.COM password: CINGULAR1 number: "*99#" network-id: 24005 device-id: da812de91eec16620b06cd0ca5cbc7ea25245222 pin: 2345 sim-id: 89148000000060671234 sim-operator-id: 310260 ``` **Requires feature: `modems`** - **`apn`** (scalar) – since 0.99 > Set the carrier APN (Access Point Name). This can be omitted if > `auto-config` is enabled. - **`auto-config`** (boolean) – since 0.99 > Specify whether to try and auto-configure the modem by doing a lookup of > the carrier against the Mobile Broadband Provider database. This may not > work for all carriers. - **`device-id`** (scalar) – since 0.99 > Specify the device ID (as given by the WWAN management service) of the > modem to match. This can be found using `mmcli`. - **`network-id`** (scalar) – since 0.99 > Specify the Network ID (GSM LAI format). If this is specified, the device > will not roam networks. - **`number`** (scalar) – since 0.99 > The number to dial to establish the connection to the mobile broadband > network. (Deprecated for GSM) - **`password`** (scalar) – since 0.99 > Specify the password used to authenticate with the carrier network. This > can be omitted if `auto-config` is enabled. - **`pin`** (scalar) – since 0.99 > Specify the SIM PIN to allow it to operate if a PIN is set. - **`sim-id`** (scalar) – since 0.99 > Specify the SIM unique identifier (as given by the WWAN management service) > which this connection applies to. If given, the connection will apply to > any device also allowed by `device-id` which contains a SIM card matching > the given identifier. - **`sim-operator-id`** (scalar) – since 0.99 > Specify the MCC/MNC string (such as `310260` or `21601`) which identifies > the carrier that this connection should apply to. If given, the connection > will apply to any device also allowed by `device-id` and `sim-id` > which contains a SIM card provisioned by the given operator. - **`username`** (scalar) – since 0.99 > Specify the username used to authenticate with the carrier network. This > can be omitted if `auto-config` is enabled. ## Properties for device type `wifis` **Status**: Optional. **Purpose**: Use the `wifis` key to configure Wi-Fi access points. **Structure**: The key consists of a mapping of Wi-Fi IDs. Each `wifi` has a number of configuration options. The general configuration structure for Wi-Fi is shown below. ```yaml network: version: 2 wifis: wlp0s1: access-points: "network_ssid_name": password: "**********" ``` Note that `systemd-networkd` does not have native support Wi-Fi, so you need wpasupplicant installed if you let the networkd renderer handle Wi-Fi. - **`access-points`** (mapping) > This provides pre-configured connections to NetworkManager. Note that > users can of course select other access points/SSIDs. The keys of the > mapping are the SSIDs, and the values are mappings with the following > supported properties: - **`password`** (scalar) > Enable WPA/WPA2 authentication and set the passphrase for it. If neither > this nor an `auth` block are given, the network is assumed to be > open. The setting > ```yaml > password: "S3kr1t" > ``` > is equivalent to > ```yaml > auth: > key-management: psk > password: "S3kr1t" > ``` - **`mode`** (scalar) > Possible access point modes are `infrastructure` (the default), > `ap` (create an access point to which other devices can connect), > and `adhoc` (peer to peer networks without a central access point). > `ap` is only supported with NetworkManager. - **`bssid`** (scalar) – since 0.99 > If specified, directs the device to only associate with the given > access point. - **`band`** (scalar) – since 0.99 > Possible bands are `5GHz` (for 5GHz 802.11a) and `2.4GHz` > (for 2.4GHz 802.11), do not restrict the 802.11 frequency band of the > network if unset (the default). - **`channel`** (scalar) – since 0.99 > Wireless channel to use for the Wi-Fi connection. Because channel > numbers overlap between bands, this property takes effect only if > the `band` property is also set. - **`hidden`** (boolean) – since 0.100 > Set to `true` to change the SSID scan technique for connecting to > hidden Wi-Fi networks. Note this may have slower performance compared > to `false` (the default) when connecting to publicly broadcast > SSIDs. - **`wakeonwlan`** (sequence of scalars) – since 0.99 > This enables WakeOnWLan on supported devices. Not all drivers support all > options. May be any combination of `any`, `disconnect`, `magic_pkt`, > `gtk_rekey_failure`, `eap_identity_req`, `four_way_handshake`, > `rfkill_release` or `tcp` (NetworkManager only). Or the exclusive > `default` flag (the default). - **`regulatory-domain`** (scalar) – since 0.105 > This can be used to define the radio's regulatory domain, to make use of > additional Wi-Fi channels outside the "world domain". Takes an ISO/ > IEC 3166 country code (like `GB`) or `00` to reset to the "world domain". > See [wireless-regdb](https://git.kernel.org/pub/scm/linux/kernel/git/sforshee/wireless-regdb.git/tree/db.txt) > for available values. > > **Requires dependency: `iw`**, if it is to be used outside the networkd > (wpasupplicant) back end. ## Properties for device type `bridges` **Status**: Optional. **Purpose**: Use the `bridges` key to create Bridge interfaces. **Structure**: The key consists of a mapping of Bridge interface names. Each `bridge` has an optional list of interfaces that will be bridged together. The interfaces listed in the `interfaces` key (`enp5s0` and `enp5s1` below) must also be defined in your Netplan configuration. The general configuration structure for Bridges is shown below. ```yaml network: bridges: br0: interfaces: - enp5s0 - enp5s1 dhcp4: true ... ``` When applied, a virtual interface of type bridge called `br0` will be created in the system. The specific settings for bridges are defined below. - **`interfaces`** (sequence of scalars) > All devices matching this ID list will be added to the bridge. This may > be an empty list, in which case the bridge will be brought online with > no member interfaces. Example: ```yaml network: ethernets: switchports: match: {name: "enp2*"} [...] bridges: br0: interfaces: [switchports] ``` - **`parameters`** (mapping) > Customisation parameters for special bridging options. Time intervals > may need to be expressed as a number of seconds or milliseconds: the > default value type is specified below. If necessary, time intervals can > be qualified using a time suffix (such as `s` for seconds, `ms` for > milliseconds) to allow for more control over its behaviour. - **`ageing-time`**, **`aging-time`** (scalar) > Set the period of time to keep a MAC address in the forwarding > database after a packet is received. This maps to the `AgeingTimeSec=` > property when the networkd renderer is used. If no time suffix is > specified, the value will be interpreted as seconds. - **`priority`** (scalar) > Set the priority value for the bridge. This value should be a > number between `0` and `65535`. Lower values mean higher > priority. The bridge with the higher priority will be elected as > the root bridge. - **`port-priority`** (mapping) > Set the port priority per interface. The priority value is > a number between `0` and `63`. This metric is used in the > designated port and root port selection algorithms. Example: ```yaml network: ethernets: eth0: dhcp4: false eth1: dhcp4: false bridges: br0: interfaces: [eth0, eth1] parameters: port-priority: eth0: 10 eth1: 20 ``` - **`forward-delay`** (scalar) > Specify the period of time the bridge will remain in Listening and > Learning states before getting to the Forwarding state. This field > maps to the `ForwardDelaySec=` property for the networkd renderer. > If no time suffix is specified, the value will be interpreted as > seconds. - **`hello-time`** (scalar) > Specify the interval between two hello packets being sent out from > the root and designated bridges. Hello packets communicate > information about the network topology. When the networkd renderer > is used, this maps to the `HelloTimeSec=` property. If no time suffix > is specified, the value will be interpreted as seconds. - **`max-age`** (scalar) > Set the maximum age of a hello packet. If the last hello packet is > older than that value, the bridge will attempt to become the root > bridge. This maps to the `MaxAgeSec=` property when the networkd > renderer is used. If no time suffix is specified, the value will be > interpreted as seconds. - **`path-cost`** (mapping) > Set the per-interface cost of a path on the bridge. Faster interfaces > should have a lower cost. This allows a finer control on the network > topology so that the fastest paths are available whenever possible. Example: ```yaml network: ethernets: eth0: dhcp4: false eth1: dhcp4: false bridges: br0: interfaces: [eth0, eth1] parameters: path-cost: eth0: 100 eth1: 200 ``` - **`stp`** (boolean) > Define whether the bridge should use Spanning Tree Protocol. The > default value is `true`, which means that Spanning Tree should be > used. ## Properties for device type `dummy-devices` **Status**: Optional. **Purpose**: Use the `dummy-devices` key to create virtual interfaces. **Structure**: The key consists of a mapping of interface names. Dummy devices are virtual devices that can be used to route packets to without actually transmitting them. ```yaml network: dummy-devices: dm0: addresses: - 192.168.0.123/24 ... ``` When applied, a virtual interface called `dm0` will be created in the system. See the ["Properties for all device types"](#properties-for-all-device-types) section for the list of properties that can be used with this type of interface. ## Properties for device type `bonds` **Status**: Optional. **Purpose**: Use the `bonds` key to create Bond (Link Aggregation) interfaces. **Structure**: The key consists of a mapping of Bond interface names. Each `bond` has an optional list of interfaces that will be part of the aggregation. The interfaces listed in the `interfaces` key must also be defined in your Netplan configuration. The general configuration structure for Bonds is shown below. ```yaml network: bonds: bond0: interfaces: - enp5s0 - enp5s1 - enp5s2 mode: active-backup ... ``` When applied, a virtual interface of type bond called `bond0` will be created in the system. The specific settings for bonds are defined below. - **`interfaces`** (sequence of scalars) > All devices matching this ID list will be added to the bond. Example: ```yaml network: ethernets: switchports: match: {name: "enp2*"} [...] bonds: bond0: interfaces: [switchports] ``` - **`parameters`** (mapping) > Customisation parameters for special bonding options. Time intervals > may need to be expressed as a number of seconds or milliseconds: the > default value type is specified below. If necessary, time intervals can > be qualified using a time suffix (such as `s` for seconds, `ms` for > milliseconds) to allow for more control over its behaviour. - **`mode`** (scalar) > Set the bonding mode used for the interfaces. The default is > `balance-rr` (round robin). Possible values are `balance-rr`, > `active-backup`, `balance-xor`, `broadcast`, `802.3ad`, > `balance-tlb` and `balance-alb`. > For Open vSwitch `active-backup` and the additional modes > `balance-tcp` and `balance-slb` are supported. - **`lacp-rate`** (scalar) > Set the rate at which LACPDUs are transmitted. This is only useful > in 802.3ad mode. Possible values are `slow` (30 seconds, default), > and `fast` (every second). - **`mii-monitor-interval`** (scalar) > Specifies the interval for MII monitoring (verifying if an interface > of the bond has carrier). The default is `0`; which disables MII > monitoring. This is equivalent to the `MIIMonitorSec=` field for the > networkd back end. If no time suffix is specified, the value will be > interpreted as milliseconds. - **`min-links`** (scalar) > The minimum number of links up in a bond to consider the bond > interface to be up. - **`transmit-hash-policy`** (scalar) > Specifies the transmit hash policy for the selection of ports. This > is only useful in `balance-xor`, `802.3ad` and `balance-tlb` modes. > Possible values are `layer2`, `layer3+4`, `layer2+3`, > `encap2+3` and `encap3+4`. - **`ad-select`** (scalar) > Set the aggregation selection mode. Possible values are `stable`, > `bandwidth` and `count`. This option is only used in 802.3ad > mode. - **`all-members-active`** (boolean) – since 0.106 > If the bond should drop duplicate frames received on inactive ports, > set this option to `false`. If they should be delivered, set this > option to `true`. The default value is false and is the desirable > behaviour in most situations. > > Alias: **`all-slaves-active`** - **`arp-interval`** (scalar) > Set the interval value for how frequently ARP link monitoring should > happen. The default value is `0`, which disables ARP monitoring. > For the networkd back end, this maps to the `ARPIntervalSec=` property. > If no time suffix is specified, the value will be interpreted as > milliseconds. - **`arp-ip-targets`** (sequence of scalars) > IP addresses of other hosts on the link which should be sent ARP requests in > order to validate that a port is up. This option is only used when > `arp-interval` is set to a value other than `0`. At least one IP > address must be given for ARP link monitoring to function. Only IPv4 > addresses are supported. You can specify up to 16 IP addresses. The > default value is an empty list. - **`arp-validate`** (scalar) > Configure how ARP replies are to be validated when using ARP link > monitoring. Possible values are `none`, `active`, `backup`, > and `all`. - **`arp-all-targets`** (scalar) > Specify whether to use any ARP IP target being up as sufficient for > a port to be considered up; or if all the targets must be up. This > is only used for `active-backup` mode when `arp-validate` is > enabled. Possible values are `any` and `all`. - **`up-delay`** (scalar) > Specify the delay before enabling a link once the link is physically > up. The default value is `0`. This maps to the `UpDelaySec=` property > for the networkd renderer. This option is only valid for the miimon > link monitor. If no time suffix is specified, the value will be > interpreted as milliseconds. - **`down-delay`** (scalar) > Specify the delay before disabling a link once the link has been > lost. The default value is `0`. This maps to the `DownDelaySec=` > property for the networkd renderer. This option is only valid for the > miimon link monitor. If no time suffix is specified, the value will > be interpreted as milliseconds. - **`fail-over-mac-policy`** (scalar) > Set whether to set all ports to the same MAC address when adding > them to the bond, or how else the system should handle MAC addresses. > The possible values are `none`, `active` and `follow`. - **`gratuitous-arp`** (scalar) > Specify how many ARP packets to send after failover. Once a link is > up on a new port, a notification is sent and possibly repeated if > this value is set to a number greater than `1`. The default value > is `1` and valid values are between `1` and `255`. This only > affects `active-backup` mode. > > For historical reasons, the misspelling `gratuitious-arp` is also > accepted and has the same function. - **`packets-per-member`** (scalar) – since 0.106 > In `balance-rr` mode, specifies the number of packets to transmit > on a port before switching to the next. When this value is set to > `0`, ports are chosen at random. Allowable values are between > `0` and `65535`. The default value is `1`. This setting is > only used in `balance-rr` mode. > > Alias: **`packets-per-slave`** - **`primary-reselect-policy`** (scalar) > Set the reselection policy for the primary port. On failure of the > active port, the system will use this policy to decide how the new > active port will be chosen and how recovery will be handled. The > possible values are `always`, `better` and `failure`. - **`resend-igmp`** (scalar) > In modes `balance-rr`, `active-backup`, `balance-tlb` and > `balance-alb`, a failover can switch IGMP traffic from one > port to another. > > This parameter specifies how many IGMP membership reports > are issued on a failover event. Values range from 0 to 255. 0 > disables sending membership reports. Otherwise, the first > membership report is sent on failover and subsequent reports > are sent at 200ms intervals. - **`learn-packet-interval`** (scalar) > Specify the interval between sending learning packets to > each port. The value range is between `1` and `0x7fffffff`. > The default value is `1`. This option only affects `balance-tlb` > and `balance-alb` modes. Using the networkd renderer, this field > maps to the `LearnPacketIntervalSec=` property. If no time suffix is > specified, the value will be interpreted as seconds. - **`primary`** (scalar) > Specify a device to be used as a primary port, or preferred device > to use as a port for the bond (i.e. the preferred device to send > data through), whenever it is available. This only affects > `active-backup`, `balance-alb` and `balance-tlb` modes. ## Properties for device type `tunnels` **Status**: Optional. **Purpose**: Use the `tunnels` key to create virtual tunnel interfaces. **Structure**: The key consists of a mapping of tunnel interface names. Each `tunnel` requires the identification of the tunnel mode (see the section `mode` below for the list of supported modes). The general configuration structure for Tunnels is shown below. ```yaml network: tunnels: tunnel0: mode: SCALAR ... ``` When applied, a virtual interface called `tunnel0` will be created in the system. Its operation mode is defined by the property `mode`. Tunnels allow traffic to pass as if it was between systems on the same local network, although systems may be far from each other but reachable via the Internet. They may be used to support IPv6 traffic on a network where the ISP does not provide the service, or to extend and "connect" separate local networks. See [Tunneling_protocol](https://en.wikipedia.org/wiki/Tunneling_protocol) for more general information about tunnels. The specific settings for tunnels are defined below. - **`mode`** (scalar) > Defines the tunnel mode. Valid options are `sit`, `gre`, `ip6gre`, > `ipip`, `ipip6`, `ip6ip6`, `vti`, `vti6`, `wireguard`, `vxlan`, > `gretap` and `ip6gretap` modes. > In addition, the `NetworkManager` back end supports `isatap` tunnels. - **`local`** (scalar) > Defines the address of the local endpoint of the tunnel. (For VXLAN) This > should match one of the parent's IP addresses or make use of the networkd > special values. - **`remote`** (scalar) > Defines the address of the remote endpoint of the tunnel or multicast group > IP address for VXLAN. - **`ttl`** (scalar) – since 0.103 > Defines the Time To Live (TTL) of the tunnel. > Takes a number in the range `1..255`. - **`key`** (scalar or mapping) > Define keys to use for the tunnel. The key can be a number or a dotted > quad (an IPv4 address). For `wireguard` it can be a base64-encoded > private key or (as of networkd v242+) an absolute path to a file, > containing the private key (since 0.100). > It is used for identification of IP transforms. This is only required > for `vti` and `vti6` when using the networkd back end. > > This field may be used as a scalar (meaning that a single key is > specified and to be used for input, output and private key), or as a > mapping, where you can further specify `input`/`output`/`private`. - **`input`** (scalar) > The input key for the tunnel - **`output`** (scalar) > The output key for the tunnel - **`private`** (scalar) – since 0.100 > A base64-encoded private key required for WireGuard tunnels. When the > `systemd-networkd` back end (v242+) is used, this can also be an > absolute path to a file containing the private key. - **`private-key-flags`** (sequence of scalars) – since 0.107 > Private key flags used by NetworkManager. Possible values are: > `agent-owned`, `not-saved` and `not-required`. > > `agent-owned`: a user-session secret agent is responsible for > providing and storing this secret. > > `not-saved`: this secret should not be saved but should be > requested from the user each time it is required. > > `not-required`: this flag hints that the secret is not required > and should not be requested from the user. Example: ```yaml network: renderer: NetworkManager tunnels: wg0: mode: wireguard port: 5182 key: private-key-flags: - agent-owned peers: - keys: public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= allowed-ips: [0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"] keepalive: 23 endpoint: 1.2.3.4:5 ``` - **`keys`** (scalar or mapping) > Alternate name for the `key` field. See above. Examples: ```yaml network: tunnels: tun0: mode: gre local: ... remote: ... keys: input: 1234 output: 5678 ``` ```yaml network: tunnels: tun0: mode: vti6 local: ... remote: ... key: 59568549 ``` ```yaml network: tunnels: wg0: mode: wireguard addresses: [...] peers: - keys: public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= shared: /path/to/shared.key ... key: mNb7OIIXTdgW4khM7OFlzJ+UPs7lmcWHV7xjPgakMkQ= ``` ```yaml network: tunnels: wg0: mode: wireguard addresses: [...] peers: - keys: public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= ... keys: private: /path/to/priv.key ``` WireGuard specific keys: - **`mark`** (scalar) – since 0.100 > Firewall mark for outgoing WireGuard packets from this interface, > optional. - **`port`** (scalar) – since 0.100 > UDP port to listen at or `auto`. Optional, defaults to `auto`. - **`peers`** (sequence of mappings) – since 0.100 > A list of peers, each having keys documented below. Example: ```yaml network: tunnels: wg0: mode: wireguard key: /path/to/private.key mark: 42 port: 5182 peers: - keys: public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= allowed-ips: [0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"] keepalive: 23 endpoint: 1.2.3.4:5 - keys: public: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= shared: /some/shared.key allowed-ips: [10.10.10.20/24] keepalive: 22 endpoint: 5.4.3.2:1 ``` - **`endpoint`** (scalar) – since 0.100 > Remote endpoint IPv4/IPv6 address or a hostname, followed by a colon > and a port number. - **`allowed-ips`** (sequence of scalars) – since 0.100 > A list of IP (v4 or v6) addresses with CIDR masks from which this peer > is allowed to send incoming traffic and to which outgoing traffic for > this peer is directed. The catch-all 0.0.0.0/0 may be specified for > matching all IPv4 addresses, and ::/0 may be specified for matching > all IPv6 addresses. - **`keepalive`** (scalar) – since 0.100 > An interval in seconds, between 1 and 65535 inclusive, of how often to > send an authenticated empty packet to the peer for the purpose of > keeping a stateful firewall or NAT mapping valid persistently. Optional. - **`keys`** (mapping) – since 0.100 > Define keys to use for the WireGuard peers. > > This field can be used as a mapping, where you can further specify the > `public` and `shared` keys. - **`public`** (scalar) – since 0.100 > A base64-encoded public key, required for WireGuard peers. - **`shared`** (scalar) – since 0.100 > A base64-encoded pre-shared key. Optional for WireGuard peers. > When the `systemd-networkd` back end (v242+) is used, this can > also be an absolute path to a file containing the pre-shared key. VXLAN specific keys: - **`id`** (scalar) – since 0.105 > The VXLAN Network Identifier (VNI or VXLAN Segment ID). > Takes a number in the range `1..16777215`. - **`link`** (scalar) – since 0.105 > Netplan ID of the parent device definition to which this VXLAN gets > connected. - **`type-of-service`** (scalar) – since 0.105 > The Type Of Service byte value for a VXLAN interface. - **`mac-learning`** (scalar) – since 0.105 > Takes a boolean. When `true`, enables dynamic MAC learning to discover > remote MAC addresses. - **`ageing`**, **`aging`** (scalar) – since 0.105 > The lifetime of Forwarding Database entry learned by the kernel, in > seconds. - **`limit`** (scalar) – since 0.105 > Configures maximum number of FDB entries. - **`arp-proxy`** (scalar) – since 0.105 > Takes a boolean. When `true`, bridge-connected VXLAN tunnel endpoint > answers ARP requests from the local bridge on behalf of remote Distributed > Overlay Virtual Ethernet (DOVE) clients. Defaults to `false`. - **`notifications`** (sequence of scalars) – since 0.105 > Takes the flags `l2-miss` and `l3-miss` to enable netlink LLADDR and/or > netlink IP address miss notifications. - **`short-circuit`** (scalar) – since 0.105 > Takes a boolean. When `true`, route short circuiting is turned on. - **`checksums`** (sequence of scalars) – since 0.105 > Takes the flags `udp`, `zero-udp6-tx`, `zero-udp6-rx`, `remote-tx` and > `remote-rx` to enable transmitting UDP checksums in VXLAN/IPv4, > send/receive zero checksums in VXLAN/IPv6 and enable sending/receiving > checksum offloading in VXLAN. - **`extensions`** (sequence of scalars) – since 0.105 > Takes the flags `group-policy` and `generic-protocol` to enable the "Group > Policy" and/or "Generic Protocol" VXLAN extensions. - **`port`** (scalar) – since 0.105 > Configures the default destination UDP port. If the destination port is > not specified then Linux kernel default will be used. Set to `4789` to get > the IANA assigned value. - **`port-range`** (sequence of scalars) – since 0.105 > Configures the source port range for the VXLAN. The kernel assigns the > source UDP port based on the flow to help the receiver to do load > balancing. When this option is not set, the normal range of local UDP > ports is used. Uses the form `[LOWER, UPPER]`. - **`flow-label`** (scalar) – since 0.105 > Specifies the flow label to use in outgoing packets. The valid range > is `0-1048575`. - **`do-not-fragment`** (scalar) – since 0.105 > Allows setting the IPv4 Do not Fragment (DF) bit in outgoing packets. > Takes a boolean value. When unset, the kernel default will be used. ## Properties for device type `virtual-ethernets` **Status**: Optional. **Purpose**: Use the `virtual-ethernets` key to create virtual Ethernet interfaces. **Structure**: The key consists of a mapping of `veth` interface names. Each `veth` requires a `peer`. In order to have a fully working `veth` pair, both devices must be defined, i.e., only setting the `peer` key with the peer name is not enough, the peer interface must also be defined and set the first one as its peer. The general configuration structure for virtual Ethernet is shown below. ```yaml network: virtual-ethernets: veth0: peer: veth1 veth1: peer: veth0 ``` When applied, two virtual interfaces called `veth0` and `veth1` will be created in the system. Virtual Ethernet devices act as tunnels forwarding traffic from one interface to the other. They can be used to connect two separate virtual networks such as network namespaces and bridges. It's not possible to move `virtual-ethernets` to different namespaces through Netplan at the present moment. The specific settings for `virtual-ethernets` are defined below. - **`peer`** (scalar) > Defines the `virtual-ethernet` peer. The peer interface must also be a `virtual-ethernet` device. Below is a complete example that uses a pair of virtual Ethernet devices to create a link between two bridges: ```yaml network: version: 2 renderer: networkd virtual-ethernets: veth0-peer1: peer: veth0-peer2 veth0-peer2: peer: veth0-peer1 bridges: br0: interfaces: - veth0-peer1 br1: interfaces: - veth0-peer2 ``` ## Properties for device type `vlans` **Status**: Optional. **Purpose**: Use the `vlans` key to create VLAN interfaces. **Structure**: The key consists of a mapping of VLAN interface names. The interface used in the `link` option (`enp5s0` in the example below) must also be defined in the Netplan configuration. The general configuration structure for VLANs is shown below. ```yaml network: vlans: vlan123: id: 123 link: enp5s0 dhcp4: yes ``` The specific settings for VLANs are defined below. - **`id`** (scalar) > VLAN ID, a number between `0` and `4094`. - **`link`** (scalar) > Netplan ID of the underlying device definition on which this VLAN gets > created. Example: ```yaml network: ethernets: eno1: {...} vlans: en-intra: id: 1 link: eno1 dhcp4: yes en-vpn: id: 2 link: eno1 addresses: [...] ``` ## Properties for device type `vrfs` **Status**: Optional. **Purpose**: Use the `vrfs` key to create Virtual Routing and Forwarding (VRF) interfaces. **Structure**: The key consists of a mapping of VRF interface names. The interface used in the `link` option (`enp5s0` in the example below) must also be defined in the Netplan configuration. The general configuration structure for VRFs is shown below. ```yaml network: renderer: networkd vrfs: vrf1: table: 1 interfaces: - enp5s0 routes: - to: default via: 10.10.10.4 routing-policy: - from: 10.10.10.42 ``` - **`table`** (scalar) – since 0.105 > The numeric routing table identifier. This setting is compulsory. - **`interfaces`** (sequence of scalars) – since 0.105 > All devices matching this ID list will be added to the VRF. This may > be an empty list, in which case the VRF will be brought online with > no member interfaces. - **`routes`** (sequence of mappings) – since 0.105 > Configure static routing for the device; see the `Routing` section. > The `table` value is implicitly set to the VRF `table`. - **`routing-policy`** (sequence of mappings) – since 0.105 > Configure policy routing for the device; see the `Routing` section. > The `table` value is implicitly set to the VRF `table`. Example: ```yaml network: vrfs: vrf20: table: 20 interfaces: [ br0 ] routes: - to: default via: 10.10.10.3 routing-policy: - from: 10.10.10.42 [...] bridges: br0: interfaces: [] ``` ## Properties for device type `nm-devices` **Status**: Optional. Its use is not recommended. **Purpose**: Use the `nm-devices` key to configure device types that are not supported by Netplan. This is NetworkManager specific configuration. **Structure**: The key consists of a mapping of NetworkManager connections. The `nm-devices` device type is for internal use only and should not be used in normal configuration files. It enables a fallback mode for unsupported settings, using the `passthrough` mapping. The general configuration structure for NM connections is shown below. ```yaml network: version: 2 nm-devices: NM-db5f0f67-1f4c-4d59-8ab8-3d278389cf87: renderer: NetworkManager networkmanager: uuid: "db5f0f67-1f4c-4d59-8ab8-3d278389cf87" name: "myvpnconnection" passthrough: connection.type: "vpn" vpn.ca: "path to ca.crt" vpn.cert: "path to client.crt" vpn.cipher: "AES-256-GCM" vpn.connection-type: "tls" vpn.dev: "tun" vpn.key: "path to client.key" vpn.remote: "1.2.3.4:1194" vpn.service-type: "org.freedesktop.NetworkManager.openvpn" ``` ## Back end-specific configuration parameters In addition to the other fields available to configure interfaces, some back ends may require to record some of their own parameters in Netplan, especially if the Netplan definitions are generated automatically by the consumer of that back end. Currently, this is only used with `NetworkManager`. - **`networkmanager`** (mapping) – since 0.99 > Keeps the NetworkManager-specific configuration parameters used by the > daemon to recognise connections. - **`name`** (scalar) – since 0.99 > Set the display name for the connection. - **`uuid`** (scalar) – since 0.99 > Defines the UUID (unique identifier) for this connection, as > generated by NetworkManager itself. - **`stable-id`** (scalar) – since 0.99 > Defines the stable ID (a different form of a connection name) used > by NetworkManager in case the name of the connection might otherwise > change, such as when sharing connections between users. - **`device`** (scalar) – since 0.99 > Defines the interface name for which this connection applies. - **`passthrough`** (mapping) – since 0.102 > Can be used as a fallback mechanism to missing key-file settings. netplan-1.0/doc/nm-all.md000066400000000000000000000020401457004145200152600ustar00rootroot00000000000000# NetworkManager default configuration Without configuration, Netplan will not do anything. Therefore, on Desktop systems, a useful configuration snippet to just bring up networking via DHCP is as follows: ```yaml network: version: 2 renderer: NetworkManager ``` This will make NetworkManager manage all devices and by default. Any Ethernet device will come up with DHCP, once carrier is detected. This is basically Netplan passing control over to NetworkManager at boot time. You can still define any more specific IDs in you Netplan configuration, to configure interfaces individually, according to Netplan [YAML reference](/netplan-yaml/). When NetworkManager [Netplan desktop integration](/netplan-everywhere/) is activated, NetworkManager will automatically create specific Netplan IDs for each of its connection profiles. This configuration snippet is shipped by default on Ubuntu Desktop systems through the [ubuntu-settings](https://launchpad.net/ubuntu/+source/ubuntu-settings) package as `/usr/lib/netplan/00-network-manager-all.yaml`. netplan-1.0/doc/reference.md000066400000000000000000000033261457004145200160460ustar00rootroot00000000000000# Reference ## YAML configuration Netplan configuration files use the [YAML (v1.1)](http://yaml.org/spec/1.1/current.html) format. All files in `/{lib,etc,run}/netplan/*.yaml` are considered and are supposed to use restrictive file permissions (`600`/`rw-------`), i.e. owner (root) read-write only. The top-level node in a Netplan configuration file is a `network:` mapping that contains `version: 2` (the YAML currently being used by curtin, MAAS, etc. is version 1), and then device definitions grouped by their type, such as `ethernets:`, `modems:`, `wifis:`, or `bridges:`. These are the types that our renderer can understand and are supported by our back ends. ```{toctree} --- maxdepth: 1 --- netplan-yaml ``` ## libnetplan API `libnetplan` is a component of the Netplan project that contains the logic for data parsing, validation and generation. It is build as a dynamic `.so` library that can be used from different binaries (like Netplan `generate`, `netplan-dbus`, the `netplan apply/try/get/set/...` CLI or using the corresponding Python bindings or external applications like the NetworkManager, using the Netplan back end). ```{toctree} libnetplan API ``` ## Netplan CLI Netplan manual pages describe the usage of the different command line interface tools available. Those are also installed on a system running Netplan and can be accessed, using the `man` utility. ```{toctree} --- maxdepth: 2 --- cli ``` ## Netplan D-Bus Netplan provides a daemon that can be run to provide the `io.netplan.Netplan` D-Bus API, to control certain aspects of a system's Netplan configuration programmatically. See also: [D-Bus configuration API](/dbus-config). ```{toctree} --- maxdepth: 1 --- Netplan D-Bus ``` netplan-1.0/doc/reuse/000077500000000000000000000000001457004145200147055ustar00rootroot00000000000000netplan-1.0/doc/reuse/links.txt000066400000000000000000000003601457004145200165650ustar00rootroot00000000000000.. _reStructuredText style guide: https://canonical-documentation-with-sphinx-and-readthedocscom.readthedocs-hosted.com/style-guide/ .. _Example product documentation: https://canonical-example-product-documentation.readthedocs-hosted.com/ netplan-1.0/doc/security.md000066400000000000000000000037071457004145200157620ustar00rootroot00000000000000--- title: "Netplan security" --- Overview of security aspects of Netplan. ## Storing credentials Credentials, such as VPN keys and Wi-Fi passwords, are stored along with the rest of the configuration in YAML files. The recommended set of file permissions is to have all YAML files owned by and only readable/writable by the root user (`chmod 600`). When using Network Manager to manage WireGuard tunnels, you can rely on an external key chain to store your private keys. For more details, see `private-key-flags` in the Netplan YAML configuration reference. :::{important} Security advice: ensure all YAML files in `/etc/netplan`, `/run/netplan` and `/lib/netplan` are not readable by non-privileged users. ::: ## Static analysis with Coverity To ensure that common issues do not sneak undetected in our code base, we scan it periodically with [Coverity](https://scan.coverity.com/). Through Coverity static analysis, we can achieve a degree of confidence that some types of issues, such as obvious memory leaks, do not stay unnoticed in the code. ## Memory issue checks As part of our CI (continuous integration) workflows, we build Netplan with the GCC address sanitiser and run unit tests and the Netplan generator against a number of YAML files. This helps us to detect issues, such as memory leaks and buffer overflows, at runtime using real configuration as input. When a memory issue is detected, the process crashes, indicating that some issue was introduced in the change. Every time a pull request is created or changes are merged to the main branch, CI executes these tests, and, if a crash happens, the workflow fails. ## Binary package hardening On Ubuntu and Debian, Netplan is built (and in fact most of the binary packages are) with a number of security flags that apply some hardening to the resulting binary. That is intended to make the life of attackers harder in case any security issue is discovered. See the `dpkg-buildflags(1)` manual page for details. netplan-1.0/doc/spelling_wordlist.txt000066400000000000000000000016211457004145200200670ustar00rootroot00000000000000abc adhoc anycast backend Backend backends blackhole bootup bugtracker cancelled checksumming checksums config ConnectX curtin dbus DBus decrypt dev dhcp dir distil dmesg empathic engreen enp enred ethernet ethernets Ethernets failover favourable Fi honoured hostname howto howtos https ifnames init initramfs instantiation io IoT ip ipip iptables ipv IPv jeopardise json keyfile Lexicographically Libera libnetplan libvirt libvirtd loopback lxd MaaS manpages Mellanox miimon minimises multicast nameservers namespaces nat natively netlink netplan networkd networkmanager NetworkManager nmconnection openvswitch ovs passthrough pre prefixtlb preshared programmatically ra recognise renderer reselection rulebook SSIDs stateful statelessly subnet subtree systemd tcp timesyncd tlb tunnelled tx udev unencrypted unicast untagged veth vlan Vlans vrf vsctl vSwitch vxlan Wi wifi wifis wpa wpasupplicant xresolve yaml netplan-1.0/doc/structure-id.md000066400000000000000000000101521457004145200165350ustar00rootroot00000000000000--- title: "Introdution to Netplan" --- Distribution installers, cloud instantiation, image builds for particular devices, or any other way to deploy an operating system put its desired network configuration into YAML configuration file(s). During early boot, the Netplan "network renderer" runs which reads `/{lib,etc,run}/netplan/*.yaml` and writes configuration to `/run` to hand off control of devices to the specified networking daemon. - Configured devices get handled by systemd-networkd by default, unless explicitly marked as managed by a specific renderer (NetworkManager) - Devices not covered by the network configuration do not get touched at all. - Usable in initramfs (few dependencies and fast) - No persistent generated configuration, only original YAML configuration - Parser supports multiple configuration files to allow applications like libvirt or `lxd` to package expected network configuration (`virbr0`, `lxdbr0`), or to change the global default policy to use NetworkManager for everything. - Retains the flexibility to change back ends/policy later or adjust to removing NetworkManager, as generated configuration is ephemeral. ## General structure Netplan configuration files use the [YAML](http://yaml.org/spec/1.1/current.html) format. All `/{lib,etc,run}/netplan/*.yaml` are considered. Lexicographically later files (regardless of in which directory they are) amend (new mapping keys) or override (same mapping keys) previous ones. A file in `/run/netplan` completely shadows a file with same name in `/etc/netplan`, and a file in either of those directories shadows a file with the same name in `/lib/netplan`. The top-level node in a Netplan configuration file is a `network:` mapping that contains `version: 2` (the YAML currently being used by curtin, MAAS, etc. is version 1), and then device definitions grouped by their type, such as `ethernets:`, `modems:`, `wifis:`, or `bridges:`. These are the types that our renderer can understand and are supported by our back ends. Each type block contains device definitions as a map where the keys (called "configuration IDs") are defined as below. ## Device configuration IDs The key names below the per-device-type definition maps (like `ethernets:`) are called "ID"s. They must be unique throughout the entire set of configuration files. Their primary purpose is to serve as anchor names for composite devices, for example to enumerate the members of a bridge that is currently being defined. (Since 0.97) If an interface is defined with an ID in a configuration file; it will be brought up by the applicable renderer. To not have Netplan touch an interface at all, it should be completely omitted from the Netplan configuration files. There are two physically/structurally different classes of device definitions, and the ID field has a different interpretation for each: Physical devices > (Examples: Ethernet, modem, Wi-Fi) These can dynamically come and go between > reboots and even during runtime (hot plugging). In the generic case, they > can be selected by `match:` rules on desired properties, such as name/name > pattern, MAC address, driver, or device paths. In general these will match > any number of devices (unless they refer to properties which are unique > such as the full path or MAC address), so without further knowledge about > the hardware these will always be considered as a group. > > It is valid to specify no match rules at all, in which case the ID field is > simply the interface name to be matched. This is mostly useful if you want > to keep simple cases simple, and it's how network device configuration has > been done for a long time. > > If there are ``match``: rules, then the ID field is a purely opaque name > which is only being used for references from definitions of compound > devices in the configuration. Virtual devices > (Examples: `veth`, `bridge`, `bond`, `vrf`) These are fully under the control of the > configuration file(s) and the network stack. I. e. these devices are being created > instead of matched. Thus `match:` and `set-name:` are not applicable for > these, and the ID field is the name of the created virtual device. netplan-1.0/doc/tutorial.md000066400000000000000000000000561457004145200157500ustar00rootroot00000000000000# Tutorial ```{toctree} netplan-tutorial ``` netplan-1.0/examples/000077500000000000000000000000001457004145200146335ustar00rootroot00000000000000netplan-1.0/examples/bonding.yaml000066400000000000000000000004271457004145200171420ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: enp3s0: {} enp4s0: {} bonds: bond0: dhcp4: yes interfaces: - enp3s0 - enp4s0 parameters: mode: active-backup primary: enp3s0 mii-monitor-interval: 100 netplan-1.0/examples/bonding_router.yaml000066400000000000000000000017151457004145200205430ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: enp1s0: dhcp4: no enp2s0: dhcp4: no enp3s0: dhcp4: no optional: true enp4s0: dhcp4: no optional: true enp5s0: dhcp4: no optional: true enp6s0: dhcp4: no optional: true bonds: bond-lan: interfaces: [enp2s0, enp3s0] addresses: [192.168.93.2/24] parameters: mode: 802.3ad mii-monitor-interval: 1 bond-wan: interfaces: [enp1s0, enp4s0] addresses: [192.168.1.252/24] nameservers: search: [local] addresses: [8.8.8.8, 8.8.4.4] parameters: mode: active-backup mii-monitor-interval: 1 gratuitious-arp: 5 routes: - to: default via: 192.168.1.1 bond-conntrack: interfaces: [enp5s0, enp6s0] addresses: [192.168.254.2/24] parameters: mode: balance-rr mii-monitor-interval: 1 netplan-1.0/examples/bridge.yaml000066400000000000000000000002341457004145200167520ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: enp3s0: dhcp4: no bridges: br0: dhcp4: yes interfaces: - enp3s0 netplan-1.0/examples/bridge_vlan.yaml000066400000000000000000000003651457004145200177770ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: enp0s25: dhcp4: true bridges: br0: addresses: [ 10.3.99.25/24 ] interfaces: [ vlan15 ] vlans: vlan15: accept-ra: no id: 15 link: enp0s25 netplan-1.0/examples/cffi-bindings.py000066400000000000000000000056711457004145200177200ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (C) 2023 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import io import tempfile from netplan import Parser, State, _create_yaml_patch from netplan import NetplanException, NetplanParserException FALLBACK_FILENAME = '70-netplan-set.yaml' # This script is a demo/example, making use of Netplan's CFFI Python bindings. # It does process your local /etc/netplan/ hierarchy, so be careful using it. # At first, it creates a Parser() object and a YAML patch, setting a # "network.ethernets.eth99.dhcp4=true" value. It loads any existing Netplan # YAML hierarcy from /etc/netplan/ and loads/applies the above mentioned patch # on top of it. Afterwards, it creates a State() object, importing parsed data # for validation and checks for any errors. # On succesful validation, it walks through all NetDefs in the validated # Netplan state and prints the Netplan ID and backend renderer of given NetDef. # Finally, it writes the validated state (including the eth99.dhcp4 setting) # back to disk in /etc/netplan/. if __name__ == '__main__': yaml_path = ['network', 'ethernets', 'eth99', 'dhcp4'] value = 'true' parser = Parser() with tempfile.TemporaryFile() as tmp: _create_yaml_patch(yaml_path, value, tmp) tmp.flush() # Parse the full, existing YAML config hierarchy parser.load_yaml_hierarchy(rootdir='/') # Load YAML patch, containing our new settings tmp.seek(0, io.SEEK_SET) parser.load_yaml(tmp) # Validate the final parser state state = State() try: # validation of current state + new settings state.import_parser_results(parser) except NetplanParserException as e: print('Error in', e.filename, 'Row/Col', e.line, e.column, '->', e.message) except NetplanException as e: print('Error:', e.message) # Walk through all NetdefIDs in the state and print their backend # renderer, to demonstrate working with NetDefinitionIterator & # NetDefinition for netdef_id, netdef in state.netdefs.items(): print('Netdef', netdef_id, 'is managed by:', netdef.backend) # Write the new data from the YAML patch to disk, updating an # existing Netdef, if file already exists, or FALLBACK_FILENAME state._update_yaml_hierarchy(FALLBACK_FILENAME, rootdir='/') netplan-1.0/examples/dhcp.yaml000066400000000000000000000001261457004145200164340ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: enp3s0: dhcp4: true netplan-1.0/examples/dhcp_wired8021x.yaml000066400000000000000000000003301457004145200203260ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: enp3s0: dhcp4: true auth: key-management: 802.1x method: ttls identity: fluffy@cisco.com password: hash:83...11 netplan-1.0/examples/direct_connect_gateway.yaml000066400000000000000000000002741457004145200222260ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: eth0: addresses: [ "10.10.10.1/24" ] routes: - to: 0.0.0.0/0 via: 9.9.9.9 on-link: true netplan-1.0/examples/direct_connect_gateway_ipv6.yaml000066400000000000000000000003311457004145200231640ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: eth0: addresses: [ "2001:cafe:face:beef::dead:dead/64" ] routes: - to: "::/0" via: "2001:cafe:face::1" on-link: true netplan-1.0/examples/dummy-devices.yaml000066400000000000000000000003131457004145200202670ustar00rootroot00000000000000network: renderer: networkd version: 2 dummy-devices: dm0: addresses: - 10.1.2.3/24 dm1: addresses: - 192.168.1.2/28 - 2001:cafe:face:beef::dead:dead/64 netplan-1.0/examples/infiniband.yaml000066400000000000000000000003251457004145200176200ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: ib0: match: macaddress: "11:22:33:44:55:66:77:88:99:00:11:22:33:44:55:66:77:88:99:00" dhcp4: true infiniband-mode: "connected" netplan-1.0/examples/ipv6_tunnel.yaml000066400000000000000000000005751457004145200177770ustar00rootroot00000000000000network: version: 2 ethernets: eth0: addresses: - 1.1.1.1/24 - "2001:cafe:face::1/64" routes: - to: default via: 1.1.1.254 tunnels: he-ipv6: mode: sit remote: 2.2.2.2 local: 1.1.1.1 addresses: - "2001:dead:beef::2/64" routes: - to: default via: "2001:dead:beef::1" netplan-1.0/examples/loopback_interface.yaml000066400000000000000000000001761457004145200213350ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: lo: match: name: lo addresses: [ 7.7.7.7/32 ] netplan-1.0/examples/meson.build000066400000000000000000000004221457004145200167730ustar00rootroot00000000000000#https://mesonbuild.com/FAQ.html#but-i-really-want-to-use-wildcards c = run_command(find, '-name', '*.yaml', check: true) examples = c.stdout().strip().split('\n') install_data( examples, install_dir: join_paths(get_option('datadir'), 'doc', 'netplan', 'examples')) netplan-1.0/examples/modem.yaml000066400000000000000000000005501457004145200166200ustar00rootroot00000000000000network: version: 2 renderer: NetworkManager modems: cdc-wdm1: mtu: 1600 apn: ISP.CINGULAR username: ISP@CINGULARGPRS.COM password: CINGULAR1 number: "*99#" network-id: 24005 device-id: da812de91eec16620b06cd0ca5cbc7ea25245222 pin: 2345 sim-id: 89148000000060671234 sim-operator-id: 310260 netplan-1.0/examples/network_manager.yaml000066400000000000000000000000611457004145200206770ustar00rootroot00000000000000network: version: 2 renderer: NetworkManager netplan-1.0/examples/offload.yaml000066400000000000000000000004631457004145200171340ustar00rootroot00000000000000network: version: 2 ethernets: ens1: receive-checksum-offload: false transmit-checksum-offload: true tcp-segmentation-offload: true tcp6-segmentation-offload: true generic-segmentation-offload: true generic-receive-offload: true large-receive-offload: true netplan-1.0/examples/openvswitch.yaml000066400000000000000000000021271457004145200200720ustar00rootroot00000000000000network: version: 2 openvswitch: protocols: [OpenFlow13, OpenFlow14, OpenFlow15] ports: - [patch0-1, patch1-0] ssl: ca-cert: /some/ca-cert.pem certificate: /another/cert.pem private-key: /private/key.pem external-ids: somekey: somevalue other-config: key: value ethernets: eth0: addresses: [10.5.32.26/20] openvswitch: external-ids: iface-id: mylocaliface other-config: disable-in-band: false eth1: {} bonds: bond0: interfaces: [patch1-0, eth1] openvswitch: lacp: passive parameters: mode: balance-tcp bridges: ovs0: addresses: [10.5.48.11/20] interfaces: [patch0-1, eth0, bond0] openvswitch: protocols: [OpenFlow10, OpenFlow11, OpenFlow12] controller: addresses: [unix:/var/run/openvswitch/ovs0.mgmt] connection-mode: out-of-band fail-mode: secure mcast-snooping: true external-ids: iface-id: myhostname other-config: disable-in-band: true netplan-1.0/examples/route_metric.yaml000066400000000000000000000002771457004145200202260ustar00rootroot00000000000000network: version: 2 ethernets: enred: dhcp4: yes dhcp4-overrides: route-metric: 100 engreen: dhcp4: yes dhcp4-overrides: route-metric: 200 netplan-1.0/examples/source_routing.yaml000066400000000000000000000010611457004145200205640ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: ens3: addresses: - 192.168.3.30/24 dhcp4: no routes: - to: 192.168.3.0/24 via: 192.168.3.1 table: 101 routing-policy: - from: 192.168.3.0/24 table: 101 ens5: addresses: - 192.168.5.24/24 dhcp4: no routes: - to: default via: 192.168.5.1 - to: 192.168.5.0/24 via: 192.168.5.1 table: 102 routing-policy: - from: 192.168.5.0/24 table: 102 netplan-1.0/examples/sriov.yaml000066400000000000000000000004531457004145200166630ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: eno1: mtu: 9000 embedded-switch-mode: "switchdev" enp1s16f1: link: eno1 addresses : [ "10.15.98.25/24" ] vf1: match: name: enp1s16f[2-3] link: eno1 addresses : [ "10.15.99.25/24" ] netplan-1.0/examples/sriov_vlan.yaml000066400000000000000000000004731457004145200177050ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: eno1: mtu: 9000 enp1s16f1: link: eno1 addresses : [ "10.15.98.25/24" ] vlans: vlan1: id: 15 link: enp1s16f1 addresses: [ "10.3.99.5/24" ] vlan2_hw: id: 10 link: enp1s16f1 renderer: sriov netplan-1.0/examples/static.yaml000066400000000000000000000004201457004145200170020ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: enp3s0: addresses: - 10.10.10.2/24 nameservers: search: [mydomain, otherdomain] addresses: [10.10.10.1, 1.1.1.1] routes: - to: default via: 10.10.10.1 netplan-1.0/examples/static_multiaddress.yaml000066400000000000000000000002771457004145200215740ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: enp3s0: addresses: - 10.100.1.38/24 - 10.100.1.39/24 routes: - to: default via: 10.100.1.1 netplan-1.0/examples/static_singlenic_multiip_multigateway.yaml000066400000000000000000000005311457004145200253770ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: eno1: addresses: - 10.0.0.10/24 - 11.0.0.11/24 nameservers: addresses: - 8.8.8.8 - 8.8.4.4 routes: - to: 0.0.0.0/0 via: 10.0.0.1 metric: 100 - to: 0.0.0.0/0 via: 11.0.0.1 metric: 200 netplan-1.0/examples/virtual-ethernet.yaml000066400000000000000000000004001457004145200210130ustar00rootroot00000000000000network: version: 2 renderer: networkd virtual-ethernets: veth0-peer1: peer: veth0-peer2 veth0-peer2: peer: veth0-peer1 bridges: br0: interfaces: - veth0-peer1 br1: interfaces: - veth0-peer2 netplan-1.0/examples/vlan.yaml000066400000000000000000000011521457004145200164560ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: mainif: match: macaddress: "de:ad:be:ef:ca:fe" set-name: mainif addresses: [ "10.3.0.5/23" ] nameservers: addresses: [ "8.8.8.8", "8.8.4.4" ] search: [ example.com ] routes: - to: default via: 10.3.0.1 vlans: vlan15: id: 15 link: mainif addresses: [ "10.3.99.5/24" ] vlan10: id: 10 link: mainif addresses: [ "10.3.98.5/24" ] nameservers: addresses: [ "127.0.0.1" ] search: [ domain1.example.com, domain2.example.com ] netplan-1.0/examples/vrf.yaml000066400000000000000000000004601457004145200163140ustar00rootroot00000000000000network: renderer: networkd vrfs: vrf1005: table: 1005 interfaces: - br1 - br1005 routes: - to: default via: 10.10.10.4 routing-policy: - from: 10.10.10.42 bridges: br1: interfaces: [] br1005: interfaces: [] netplan-1.0/examples/vxlan.yaml000066400000000000000000000012551457004145200166520ustar00rootroot00000000000000network: renderer: networkd ethernets: lo: addresses: - 192.168.10.10/32 vrfs: vrf1005: table: 1005 interfaces: - br1 - br1005 bridges: br1: interfaces: - vxlan1 br1005: interfaces: - vxlan1005 tunnels: vxlan1005: mode: vxlan id: 1005 link: lo mtu: 8950 accept-ra: no neigh-suppress: true mac-learning: false port: 4789 local: 192.168.10.10 vxlan1: mode: vxlan id: 1 link: lo mtu: 8950 accept-ra: no neigh-suppress: true mac-learning: false port: 4789 local: 192.168.10.10 netplan-1.0/examples/windows_dhcp_server.yaml000066400000000000000000000001331457004145200215720ustar00rootroot00000000000000network: version: 2 ethernets: enp3s0: dhcp4: yes dhcp-identifier: mac netplan-1.0/examples/wireguard.yaml000066400000000000000000000016671457004145200175220ustar00rootroot00000000000000network: version: 2 tunnels: wg0: #server mode: wireguard addresses: [10.10.10.20/24] key: 4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8= mark: 42 port: 51820 peers: - keys: public: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= shared: 7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= allowed-ips: [20.20.20.10/24] routes: - to: default via: 10.10.10.21 metric: 100 wg1: #client mode: wireguard addresses: [20.20.20.10/24] key: KPt9BzQjejRerEv8RMaFlpsD675gNexELOQRXt/AcH0= peers: - endpoint: 10.10.10.20:51820 allowed-ips: [0.0.0.0/0] keys: public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= shared: 7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= keepalive: 21 routes: - to: default via: 20.20.20.11 metric: 200 netplan-1.0/examples/wireless.yaml000066400000000000000000000005621457004145200173570ustar00rootroot00000000000000network: version: 2 renderer: networkd wifis: wlp2s0b1: regulatory-domain: "GB" dhcp4: no dhcp6: no addresses: [192.168.0.21/24] nameservers: addresses: [192.168.0.1, 8.8.8.8] access-points: "network_ssid_name": password: "**********" routes: - to: default via: 192.168.0.1 netplan-1.0/examples/wireless_adhoc.yaml000066400000000000000000000006731457004145200205200ustar00rootroot00000000000000network: version: 2 wifis: wl0: link-local: [ ipv4 ] access-points: "test": mode: adhoc band: 2.4GHz channel: 7 password: "********" wl1: addresses: [192.168.2.12/24] routes: - to: default via: 192.168.2.1 access-points: "test": mode: adhoc band: 2.4GHz channel: 7 password: "********" netplan-1.0/examples/wireless_wpa3.yaml000066400000000000000000000002551457004145200203100ustar00rootroot00000000000000network: wifis: wlp1s0: dhcp4: true access-points: "WPA3-Network": auth: key-management: sae password: abcdefgh netplan-1.0/examples/wpa3_enterprise.yaml000066400000000000000000000015771457004145200206430ustar00rootroot00000000000000network: version: 2 wifis: wl0: dhcp4: yes access-points: university: auth: key-management: eap-sha256 method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "d3cryptPr1v4t3K3y" enterprise: auth: key-management: eap-suite-b-192 method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "d3cryptPr1v4t3K3y" netplan-1.0/examples/wpa_enterprise.yaml000066400000000000000000000014141457004145200205460ustar00rootroot00000000000000network: version: 2 wifis: wl0: access-points: workplace: auth: key-management: eap method: ttls anonymous-identity: "@internal.example.com" identity: "joe@internal.example.com" password: "v3ryS3kr1t" university: auth: key-management: eap method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "d3cryptPr1v4t3K3y" open-network: auth: key-management: none dhcp4: yes netplan-1.0/features_h_generator.sh000077500000000000000000000005311457004145200175460ustar00rootroot00000000000000#!/bin/sh BASE=$(dirname $0) OUTPUT=$BASE/src/_features.h INPUT=$BASE/src/[!_]*.[hc] printf "#include \nstatic const char *feature_flags[] __attribute__((__unused__)) = {\n" > $OUTPUT awk 'match ($0, /netplan-feature:.*/ ) { $0=substr($0, RSTART, RLENGTH); print "\""$2"\"," }' $INPUT >> $OUTPUT echo "NULL, };" >> $OUTPUT cat $OUTPUT netplan-1.0/features_py_generator.sh000077500000000000000000000005001457004145200177430ustar00rootroot00000000000000#!/bin/sh BASE=$(dirname $0) OUTPUT=$BASE/netplan_cli/_features.py INPUT=$BASE/src/[!_]*.[hc] echo "# Generated file" > $OUTPUT echo "NETPLAN_FEATURE_FLAGS = [" >> $OUTPUT awk 'match ($0, /netplan-feature:.*/ ) { $0=substr($0, RSTART, RLENGTH); print " \""$2"\"," }' $INPUT >> $OUTPUT echo "]" >> $OUTPUT cat $OUTPUT netplan-1.0/gcovr.cfg000066400000000000000000000000471457004145200146170ustar00rootroot00000000000000filter = src/* filter = tests/ctests/* netplan-1.0/include/000077500000000000000000000000001457004145200144405ustar00rootroot00000000000000netplan-1.0/include/meson.build000066400000000000000000000002261457004145200166020ustar00rootroot00000000000000install_headers( 'netdef.h', 'netplan.h', 'parse.h', 'parse-nm.h', 'state.h', 'types.h', 'util.h', subdir: 'netplan') netplan-1.0/include/netdef.h000066400000000000000000000257501457004145200160670ustar00rootroot00000000000000/* * Copyright (C) 2024 Canonical, Ltd. * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * @file netdef.h * @brief Functions for manipulating @ref NetplanNetDefinition objects and querying properties of individual Netplan IDs. */ #pragma once #include #include "types.h" /** * @brief Get the full path that a @ref NetplanNetDefinition will be written to by its backend renderer. * @details Copies a `NUL`-terminated string into a sized @p out_buffer. If the * buffer is too small, its content is not `NUL`-terminated. * @note Used by the NetworkManager YAML backend but also applicable to the systemd-networkd renderer. * @param[in] netdef The @ref NetplanNetDefinition to query * @param[in] ssid Wi-Fi SSID of this connection, or `NULL` * @param[out] out_buffer A pre-allocated buffer to write the output string into, owned by the caller * @param[in] out_buf_size The maximum size (in bytes) available for @p out_buffer * @return The size of the copied string, including the final `NUL` character. * If the buffer is too small, returns @ref NETPLAN_BUFFER_TOO_SMALL instead. */ NETPLAN_PUBLIC ssize_t netplan_netdef_get_output_filename(const NetplanNetDefinition* netdef, const char* ssid, char* out_buffer, size_t out_buf_size); /** * @brief Get the origin filepath of a given @ref NetplanNetDefinition. * @details Copies a `NUL`-terminated string into a sized @p out_buffer. If the * buffer is too small, its content is not `NUL`-terminated. * @param[in] netdef The @ref NetplanNetDefinition to query * @param[out] out_buffer A pre-allocated buffer to write the output string into, owned by the caller * @param[in] out_buffer_size The maximum size (in bytes) available for @p out_buffer * @return The size of the copied string, including the final `NUL` character. * If the buffer is too small, returns @ref NETPLAN_BUFFER_TOO_SMALL instead. */ NETPLAN_PUBLIC ssize_t netplan_netdef_get_filepath(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buffer_size); /** * @brief Get the specific @ref NetplanBackend defined for this @ref NetplanNetDefinition. * @param[in] np_state The @ref NetplanState to query * @return Enumeration value, specifiying the @ref NetplanBackend */ NETPLAN_PUBLIC NetplanBackend netplan_netdef_get_backend(const NetplanNetDefinition* netdef); /** * @brief Get the interface type for a given @ref NetplanNetDefinition. * @param[in] np_state The @ref NetplanState to query * @return Enumeration value of @ref NetplanDefType, specifiying the interface type */ NETPLAN_PUBLIC NetplanDefType netplan_netdef_get_type(const NetplanNetDefinition* netdef); /** * @brief Get the Netplan ID of a given @ref NetplanNetDefinition. * @details Copies a `NUL`-terminated string into a sized @p out_buffer. If the * buffer is too small, its content is not `NUL`-terminated. * @param[in] netdef The @ref NetplanNetDefinition to query * @param[out] out_buffer A pre-allocated buffer to write the output string into, owned by the caller * @param[in] out_buffer_size The maximum size (in bytes) available for @p out_buffer * @return The size of the copied string, including the final `NUL` character. * If the buffer is too small, returns @ref NETPLAN_BUFFER_TOO_SMALL instead. */ NETPLAN_PUBLIC ssize_t netplan_netdef_get_id(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buffer_size); /** * @brief Get a reference to a linked @ref NetplanNetDefinition for a given @p netdef. * @details This defines the parent-child relationship between bridged interfaces. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Reference to the parent of @p netdef */ NETPLAN_PUBLIC NetplanNetDefinition* netplan_netdef_get_bridge_link(const NetplanNetDefinition* netdef); /** * @brief Get a reference to a linked @ref NetplanNetDefinition for a given @p netdef. * @details This defines the parent-child relationship between bonded interfaces. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Reference to the parent of @p netdef */ NETPLAN_PUBLIC NetplanNetDefinition* netplan_netdef_get_bond_link(const NetplanNetDefinition* netdef); /** * @brief Get a reference to a linked @ref NetplanNetDefinition for a given @p netdef. * @details This defines the peer relationship between veth or Open vSwitch interfaces. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Reference to the peer of @p netdef */ NETPLAN_PUBLIC NetplanNetDefinition* netplan_netdef_get_peer_link(const NetplanNetDefinition* netdef); /** * @brief Get a reference to a linked @ref NetplanNetDefinition for a given @p netdef. * @details This defines the parent-child relationship of VLAN interfaces. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Reference to the parent of @p netdef */ NETPLAN_PUBLIC NetplanNetDefinition* netplan_netdef_get_vlan_link(const NetplanNetDefinition* netdef); /** * @brief Get a reference to a linked @ref NetplanNetDefinition for a given @p netdef. * @details This defines the parent-child relationship of SR-IOV virtual functions. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Reference to the physical function of @p netdef */ NETPLAN_PUBLIC NetplanNetDefinition* netplan_netdef_get_sriov_link(const NetplanNetDefinition* netdef); /** * @brief Get a reference to a linked @ref NetplanNetDefinition for a given @p netdef. * @details This defines the parent-child relationship of VRF interfaces. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Reference to the parent of @p netdef */ NETPLAN_PUBLIC NetplanNetDefinition* netplan_netdef_get_vrf_link(const NetplanNetDefinition* netdef); /** * @brief Get the `set-name` setting of a given @ref NetplanNetDefinition. * @details Copies a `NUL`-terminated string into a sized @p out_buffer. If the * buffer is too small, its content is not `NUL`-terminated. * @note This is unrelated to the `match.name` setting. * @param[in] netdef The @ref NetplanNetDefinition to query * @param[out] out_buffer A pre-allocated buffer to write the output string into, owned by the caller * @param[in] out_buffer_size The maximum size (in bytes) available for @p out_buffer * @return The size of the copied string, including the final `NUL` character. * If the buffer is too small, returns @ref NETPLAN_BUFFER_TOO_SMALL instead. */ NETPLAN_PUBLIC ssize_t netplan_netdef_get_set_name(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buffer_size); /** * @brief Query a @ref NetplanNetDefinition for a `match` stanza in its configuration. * @details In the absence of a `match` stanza, the Netplan ID can be considered to be the interface name of a single interface. Otherwise, it could match multiple interfaces. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Indication if @p netdef uses custom interface matching rules */ NETPLAN_PUBLIC gboolean netplan_netdef_has_match(const NetplanNetDefinition* netdef); /** * @brief Check if a @ref NetplanNetDefinition matches on given interface parameters. * @details If defined in @p netdef, calculate if it would match on given @p mac AND @p name AND @p driver_name parameters. * @note Matching a single driver out of a list given in the YAML configuration is enough to satisfy the condition. * @param[in] netdef The @ref NetplanNetDefinition to query * @param[in] name The interface name match, optionally using shell wildcard patterns (`fnmatch()`) * @param[in] mac The exact, case insensitive match on the interface MAC address * @param[in] driver_name The driver match, optionally using shell wildcard patterns (`fnmatch()`) * @return Indication if @p netdef uses custom interface matching rules */ NETPLAN_PUBLIC gboolean netplan_netdef_match_interface(const NetplanNetDefinition* netdef, const char* name, const char* mac, const char* driver_name); /** * @brief Query a @ref NetplanNetDefinition for the value of its `dhcp4` setting. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Indication if @p netdef is configured to enable DHCP for IPv4 */ NETPLAN_PUBLIC gboolean netplan_netdef_get_dhcp4(const NetplanNetDefinition* netdef); /** * @brief Query a @ref NetplanNetDefinition for the value of its `dhcp6` setting. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Indication if @p netdef is configured to enable DHCP for IPv6 */ NETPLAN_PUBLIC gboolean netplan_netdef_get_dhcp6(const NetplanNetDefinition* netdef); /** * @brief Query a @ref NetplanNetDefinition for the value of its `link-local` setting for IPv4. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Indication if @p netdef is configured to enable the link-local address for IPv4 */ NETPLAN_PUBLIC gboolean netplan_netdef_get_link_local_ipv4(const NetplanNetDefinition* netdef); /** * @brief Query a @ref NetplanNetDefinition for the value of its `link-local` setting for IPv6. * @param[in] netdef The @ref NetplanNetDefinition to query * @return Indication if @p netdef is configured to enable the link-local address for IPv6 */ NETPLAN_PUBLIC gboolean netplan_netdef_get_link_local_ipv6(const NetplanNetDefinition* netdef); /** * @brief Get the `macaddress` setting of a given @ref NetplanNetDefinition. * @details Copies a `NUL`-terminated string into a sized @p out_buffer. If the * buffer is too small, its content is not `NUL`-terminated. * @note This is unrelated to the `match.macaddress` setting. * @param[in] netdef The @ref NetplanNetDefinition to query * @param[out] out_buffer A pre-allocated buffer to write the output string into, owned by the caller * @param[in] out_buffer_size The maximum size (in bytes) available for @p out_buffer * @return The size of the copied string, including the final `NUL` character. * If the buffer is too small, returns @ref NETPLAN_BUFFER_TOO_SMALL instead. */ NETPLAN_PUBLIC ssize_t netplan_netdef_get_macaddress(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buffer_size); netplan-1.0/include/netplan.h000066400000000000000000000015461457004145200162600ustar00rootroot00000000000000/* * Copyright (C) 2021-2024 Canonical, Ltd. * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * @file netplan.h * @brief Aggregation of our other header files. */ #pragma once #include "parse.h" #include "parse-nm.h" #include "state.h" #include "netdef.h" #include "util.h" netplan-1.0/include/parse-nm.h000066400000000000000000000031221457004145200163310ustar00rootroot00000000000000/* * Copyright (C) 2021-2024 Canonical, Ltd. * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * @file parse-nm.h * @brief Parsing NetworkManager keyfile configuration into @ref NetplanParser data structures. */ #pragma once #include "types.h" /// The value `_` indicates an emtpy keyfile group in `networkmanager.passthrough` handling. #define NETPLAN_NM_EMPTY_GROUP "_" /** * @brief Parse a NetworkManager keyfile into a @ref NetplanNetDefinition struct. * @param[in] npp The @ref NetplanParser object that should contain the parsed data * @param[in] filename Full path to the NetworkManager keyfile * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_parser_load_keyfile(NetplanParser* npp, const char* filename, NetplanError** error); //TODO: needs to be implemented //NETPLAN_PUBLIC gboolean //netplan_parser_load_keyfile_from_fd(NetplanParser* npp, int input_fd, NetplanError** error); netplan-1.0/include/parse.h000066400000000000000000000131371457004145200157300ustar00rootroot00000000000000/* * Copyright (C) 2016-2024 Canonical, Ltd. * Author: Martin Pitt * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * @file parse.h * @brief Parsing Netplan YAML configuration into @ref NetplanParser data structures. */ #pragma once #include #include "types.h" /**************************************************** * Functions ****************************************************/ /** * @brief Allocate and initialize a new @ref NetplanParser object. * @note This contains unvalidated Netplan configuration from raw input, like Netplan YAML or NetworkManager keyfile. * @return An empty @ref NetplanParser */ NETPLAN_PUBLIC NetplanParser* netplan_parser_new(); /** * @brief Reset a @ref NetplanParser to its initial default values. * @details Freeing any dynamically allocated parsing data. * @param[in] npp The @ref NetplanParser to be reset */ NETPLAN_PUBLIC void netplan_parser_reset(NetplanParser *npp); /** * @brief Free a @ref NetplanParser, including any dynamically allocated data. * @details Similar to @ref netplan_parser_reset, but also free and nullify the object itself. * @param[out] npp The @ref NetplanParser to free and nullify */ NETPLAN_PUBLIC void netplan_parser_clear(NetplanParser **npp); /** * @brief Parse a given YAML file and create or update the list of @ref NetplanNetDefinition inside @p npp. * @param[in] npp The @ref NetplanParser object that should contain the parsed data * @param[in] filename Full path to a Netplan YAML configuration file * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_parser_load_yaml(NetplanParser* npp, const char* filename, NetplanError** error); /** * @brief Parse a given YAML file from a file descriptor and create or update the list of @ref NetplanNetDefinition inside @p npp. * @param[in] npp The @ref NetplanParser object that should contain the parsed data * @param[in] input_fd File descriptor reference to a Netplan YAML configuration file * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_parser_load_yaml_from_fd(NetplanParser* npp, int input_fd, NetplanError** error); /** * @brief Parse a full hierarchy of `/{usr/lib,etc,run}/netplan/\*.yaml` files inside * @p rootdir and create or update the list of @ref NetplanNetDefinition inside @p npp. * @note Files with "asciibetically" higher names override/append settings from earlier ones * (in all Netplan config directories); files in `/run/netplan/` shadow files in * `/etc/netplan/`, which shadow files in `/usr/lib/netplan/`. * @param[in] npp The @ref NetplanParser object that should contain the parsed data * @param[in] rootdir If not `NULL`, parse configuration from this root directory (useful for testing) * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_parser_load_yaml_hierarchy(NetplanParser* npp, const char* rootdir, NetplanError** error); /** * @brief Parse a Netplan YAML configuration file from a file descriptor, containing settings * that are about to be deleted (e.g. `some.setting=NULL`). * @details The `NULL`-settings are ignored when parsing subsequent YAML files. * @param[in] npp The @ref NetplanParser object that should contain the parsed data * @param[in] input_fd File descriptor reference to a Netplan YAML configuration file * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_parser_load_nullable_fields(NetplanParser* npp, int input_fd, NetplanError** error); /** * @brief Parse a Netplan YAML configuration file from a file descriptor, containing special settings that * are to be overriden inside the YAML hierarchy by the resulting "origin-hint" output file. * @details Global settings (like `renderer`) or @ref NetplanNetDefinition, defined in @p input_fd * are ignored in the existing YAML hierarchy because the @p input_fd configuration * overrides those settings via the "origin-hint" output file. * @note Those settings are supposed to be parsed from the "origin-hint" output file given in @p constraint only. * @param[in] npp The @ref NetplanParser object that should contain the parsed data * @param[in] input_fd File descriptor reference to a Netplan YAML configuration file, which would become the "origin-hint" output file afterwards * @param[in] constraint Basename of the "origin-hint" output file * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_parser_load_nullable_overrides( NetplanParser* npp, int input_fd, const char* constraint, NetplanError** error); netplan-1.0/include/state.h000066400000000000000000000234541457004145200157410ustar00rootroot00000000000000/* * Copyright (C) 2021-2024 Canonical, Ltd. * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * @file state.h * @brief Functions for manipulating @ref NetplanState objects, validating * Netplan configurations and writing them to disk. */ #pragma once #include #include "types.h" /** * @brief Allocate and initialize a new @ref NetplanState object. * @details Can be used to validate and carry pre-parsed Netplan configuration. * @return An empty @ref NetplanState */ NETPLAN_PUBLIC NetplanState* netplan_state_new(); /** * @brief Reset a @ref NetplanState to its initial default values. * @details Freeing any dynamically allocated configuration data. * @param[in] np_state The @ref NetplanState to be reset */ NETPLAN_PUBLIC void netplan_state_reset(NetplanState* np_state); /** * @brief Free a @ref NetplanState, including any dynamically allocated data. * @details Similar to @ref netplan_state_reset, but also free and nullify the object itself. * @param[out] np_state The @ref NetplanState to free and nullify */ NETPLAN_PUBLIC void netplan_state_clear(NetplanState** np_state); /** * @brief Validate pre-parsed Netplan configuration data inside a @ref NetplanParser and import them into a @ref NetplanState. * @details This transfers ownership of the contained data from @p npp to @p np_state and cleans up by calling @ref netplan_parser_reset. * @param[in] np_state The @ref NetplanState to be filled with validated Netplan configuration from @p npp * @param[in] npp The @ref NetplanParser containing unvalidated Netplan configuration from raw inputs * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_state_import_parser_results(NetplanState* np_state, NetplanParser* npp, NetplanError** error); /** * @brief Get the number of @ref NetplanNetDefinition configurations stored in this @ref NetplanState * @note Each @ref NetplanNetDefinition is identified by a unique Netplan ID. * @param[in] np_state The @ref NetplanState to query * @return Number of unique @ref NetplanNetDefinition configurations */ NETPLAN_PUBLIC guint netplan_state_get_netdefs_size(const NetplanState* np_state); /** * @brief Get a specific @ref NetplanNetDefinition from this @ref NetplanState * @param[in] np_state The @ref NetplanState to query * @param[in] id The unique Netplan ID, referencing a @ref NetplanNetDefinition * @return A handle to the specified @ref NetplanNetDefinition or `NULL` if not found */ NETPLAN_PUBLIC NetplanNetDefinition* netplan_state_get_netdef(const NetplanState* np_state, const char* id); /** * @brief Get the global @ref NetplanBackend defined in this @ref NetplanState. * @note This is the default fallback backend to render any contained @ref NetplanNetDefinition on, if not otherwise specified. * @param[in] np_state The @ref NetplanState to query * @return Enumeration value, specifiying the @ref NetplanBackend */ NETPLAN_PUBLIC NetplanBackend netplan_state_get_backend(const NetplanState* np_state); /** * @brief Write the selected YAML file filtered to the data relevant to this file. * @details Writes all @ref NetplanNetDefinition settings that originate from the specified file, * as well as those without any given origin. Any data that's assigned to another file is ignored. * @param[in] np_state The @ref NetplanState for which to generate the config * @param[in] filename Relevant file basename (e.g. `origin-hint.yaml`) * @param[in] rootdir If not `NULL`, generate configuration in this root directory (useful for testing) * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_state_write_yaml_file( const NetplanState* np_state, const char* filename, const char* rootdir, NetplanError** error); /** * @brief Update all the YAML files that were used to create this @ref NetplanState. * @details Data that has no associated filepath uses the @p default_filename * output file in the standard configuration directory. * @param[in] np_state The @ref NetplanState for which to generate the configuration * @param[in] default_filename Default configuration file; cannot be `NULL` or empty * @param[in] rootdir If not `NULL`, generate configuration in this root directory (useful for testing) * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_state_update_yaml_hierarchy( const NetplanState* np_state, const char* default_filename, const char* rootdir, NetplanError** error); /** * @brief Dump the whole @ref NetplanState into a single YAML file. * @details Ignoring the origin of each @ref NetplanNetDefinition. * @param[in] np_state The @ref NetplanState for which to generate the configuration * @param[in] out_fd File descriptor to an opened file into which to dump the content * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_state_dump_yaml( const NetplanState* np_state, int output_fd, NetplanError** error); /** * @brief Generate the Netplan YAML configuration for the selected @ref NetplanNetDefinition. * @param[in] np_state @ref NetplanState (as pointer), the global state to which the `netdef` belongs * @param[in] netdef @ref NetplanNetDefinition (as pointer), the data to be serialized * @param[in] rootdir If not `NULL`, generate configuration in this root directory (useful for testing) * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_netdef_write_yaml( const NetplanState* np_state, const NetplanNetDefinition* netdef, const char* rootdir, NetplanError** error); /** * @brief Initialize a @ref NetplanStateIterator for walking through a list of @ref NetplanNetDefinition inside @p np_state. * @param[in] np_state The @ref NetplanState to query * @param[in,out] iter A @ref NetplanStateIterator structure to be initialized */ NETPLAN_PUBLIC void netplan_state_iterator_init(const NetplanState* np_state, NetplanStateIterator* iter); /** * @brief Get the next @ref NetplanNetDefinition in the list of a @ref NetplanState object. * @param[in,out] iter A @ref NetplanStateIterator to work with * @return The next @ref NetplanNetDefinition or `NULL` */ NETPLAN_PUBLIC NetplanNetDefinition* netplan_state_iterator_next(NetplanStateIterator* iter); /** * @brief Check if there is any next @ref NetplanNetDefinition in the list of a @ref NetplanState object. * @param[in,out] iter A @ref NetplanStateIterator to work with * @return Indication if this @ref NetplanStateIterator contains any further @ref NetplanNetDefinition */ NETPLAN_PUBLIC gboolean netplan_state_iterator_has_next(const NetplanStateIterator* iter); /** * @brief Write generic NetworkManager configuration to disk. * @details This configures global settings, independent of @ref NetplanNetDefinition data, such as udev blocklisting to make NetworkManager ignore certain interfaces using `[device].managed=false` or `NM_MANAGED=0`. * @param[in] np_state The @ref NetplanState to read settings from * @param[in] rootdir If not `NULL`, generate configuration in this root directory (useful for testing) * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_state_finish_nm_write( const NetplanState* np_state, const char* rootdir, NetplanError** error); /** * @brief Write generic Open vSwitch configuration to disk. * @details This configures global settings, independent of @ref NetplanNetDefinition data, such as patch ports, SSL configuration or the `netplan-ovs-cleanup.service` unit. * @param[in] np_state The @ref NetplanState to read settings from * @param[in] rootdir If not `NULL`, generate configuration in this root directory (useful for testing) * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_state_finish_ovs_write( const NetplanState* np_state, const char* rootdir, NetplanError** error); /** * @brief Write generic SR-IOV configuration to disk. * @details This configures global settings, independent of @ref NetplanNetDefinition data, such as udev rules or the `netplan-sriov-rebind.service` unit. * @param[in] np_state The @ref NetplanState to read settings from * @param[in] rootdir If not `NULL`, generate configuration in this root directory (useful for testing) * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_state_finish_sriov_write( const NetplanState* np_state, const char* rootdir, NetplanError** error); netplan-1.0/include/types.h000066400000000000000000000134211457004145200157560ustar00rootroot00000000000000/* * Copyright (C) 2022-2024 Canonical, Ltd. * Author: Danilo Egea Gondolfo * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * @file types.h * @brief Definition of public Netplan types. */ #pragma once /// Symbols that are considered part of Netplan public API. #define NETPLAN_PUBLIC __attribute__ ((visibility("default"))) /// Symbols that are used internally by Netplan. /// @warning Do not use those symbols in an external codebase, they might be dropped or changed without notice. #define NETPLAN_INTERNAL __attribute__ ((visibility("default"))) /// Symbols that are outdated and should not be used anymore. /// @note Those symbols will be dropped in the future. #define NETPLAN_DEPRECATED __attribute__ ((deprecated)) /// Error of value `-2` to indicate an issue with the buffer. #define NETPLAN_BUFFER_TOO_SMALL -2 /**************************************************** * Parsed definitions ****************************************************/ #include /// Network interface types supported by Netplan. typedef enum { NETPLAN_DEF_TYPE_NONE, /* physical devices */ NETPLAN_DEF_TYPE_ETHERNET, NETPLAN_DEF_TYPE_WIFI, NETPLAN_DEF_TYPE_MODEM, /* virtual devices */ NETPLAN_DEF_TYPE_VIRTUAL, NETPLAN_DEF_TYPE_BRIDGE = NETPLAN_DEF_TYPE_VIRTUAL, NETPLAN_DEF_TYPE_BOND, NETPLAN_DEF_TYPE_VLAN, NETPLAN_DEF_TYPE_TUNNEL, NETPLAN_DEF_TYPE_PORT, NETPLAN_DEF_TYPE_VRF, /* Type fallback/passthrough */ NETPLAN_DEF_TYPE_NM, NETPLAN_DEF_TYPE_DUMMY, /* wokeignore:rule=dummy */ NETPLAN_DEF_TYPE_VETH, /* Place holder type used to fill gaps when a netdef * requires links to another netdef (such as vlan_link) * but it's not strictly mandatory * It's intended to be used only when renderer is NetworkManager */ NETPLAN_DEF_TYPE_NM_PLACEHOLDER_, NETPLAN_DEF_TYPE_MAX_ } NetplanDefType; /// Private data structure to contain parsed but unvalidated Netplan configuration. /// See @ref netplan_parser_new and related accessor functions. typedef struct netplan_parser NetplanParser; /// Private data structure to contain validated Netplan configuration, ready for writing to disk. /// See @ref netplan_state_new and related accessor functions. typedef struct netplan_state NetplanState; /// Private data structure to contain individual settings per Netplan ID. /// See @ref netplan_state_get_netdef, @ref netplan_netdef_get_id and related accessor functions. typedef struct netplan_net_definition NetplanNetDefinition; /// Renderer backends supported by Netplan. typedef enum { NETPLAN_BACKEND_NONE, NETPLAN_BACKEND_NETWORKD, NETPLAN_BACKEND_NM, NETPLAN_BACKEND_OVS, NETPLAN_BACKEND_MAX_, } NetplanBackend; /// Private data structure for error reporting. /// See @ref netplan_error_code, @ref netplan_error_message and @ref netplan_error_clear. typedef GError NetplanError; /// Private data structure to iterate through a list of @ref NetplanNetDefinition inside @ref NetplanState. /// See @ref netplan_state_iterator_init and related accessor functions. typedef struct _NetplanStateIterator NetplanStateIterator; /** * @brief Defining a non-opaque placeholder type for the private `struct netplan_state_iterator`. * @details Do not use directly. Use @ref NetplanStateIterator instead. Enables consumers to place the iterator at the stack. * @note The idea is based on the GLib implementation of iterators. */ struct _NetplanStateIterator { void* placeholder; }; /* * Errors and error domains * * NOTE: if new errors or domains are added, * python-cffi/netplan/_utils.py must be updated with the new entries. */ /// Defining different classes of @ref NetplanError. enum NETPLAN_ERROR_DOMAINS { NETPLAN_PARSER_ERROR = 1, ///< See @ref NETPLAN_PARSER_ERRORS NETPLAN_VALIDATION_ERROR, ///< See @ref NETPLAN_VALIDATION_ERRORS NETPLAN_FILE_ERROR, ///< Returns `errno` as the @ref NetplanError code and a corresponding message. NETPLAN_BACKEND_ERROR, ///< See @ref NETPLAN_BACKEND_ERRORS NETPLAN_EMITTER_ERROR, ///< See @ref NETPLAN_EMITTER_ERRORS NETPLAN_FORMAT_ERROR, ///< See @ref NETPLAN_FORMAT_ERRORS }; /** * @brief Errors for domain @ref NETPLAN_PARSER_ERROR. * @details Such errors are expected to contain the file name, * line and column numbers. */ enum NETPLAN_PARSER_ERRORS { NETPLAN_ERROR_INVALID_YAML, NETPLAN_ERROR_INVALID_CONFIG }; /** * @brief Errors for domain @ref NETPLAN_VALIDATION_ERROR. * @details Such errors are expected to contain only the YAML file name * where the error was found. */ enum NETPLAN_VALIDATION_ERRORS { NETPLAN_ERROR_CONFIG_GENERIC, NETPLAN_ERROR_CONFIG_VALIDATION, }; /// @brief Errors for domain @ref NETPLAN_BACKEND_ERROR. enum NETPLAN_BACKEND_ERRORS { NETPLAN_ERROR_UNSUPPORTED, NETPLAN_ERROR_VALIDATION, }; /// @brief Errors for domain @ref NETPLAN_EMITTER_ERROR. enum NETPLAN_EMITTER_ERRORS { NETPLAN_ERROR_YAML_EMITTER, }; /** * @brief Errors for domain @ref NETPLAN_FORMAT_ERROR. * @details Such errors are generic errors emitted from contexts where information * such as the file name is not known. */ enum NETPLAN_FORMAT_ERRORS { NETPLAN_ERROR_FORMAT_INVALID_YAML, }; netplan-1.0/include/util.h000066400000000000000000000143261457004145200155740ustar00rootroot00000000000000/* * Copyright (C) 2016-2024 Canonical, Ltd. * Author: Martin Pitt * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * @file util.h * @brief A set of generic helper functions that can be used when working with * Netplan data structures. Such as handling of @ref NetplanError data. */ #pragma once #include #include #include "types.h" /** * @brief Parses YAML hierarchy from @p rootdir, drops the configuration for @p id * from the @ref NetplanState and re-generates the YAML files. * @param[in] id The Netplan ID for a specific configuration block of network interface(s) * @param[in] rootdir If not `NULL`, parse configuration from this root directory (useful for testing) * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_delete_connection(const char* id, const char* rootdir); /** * @brief Extract the Netplan ID from the filepath of a NetworkManager keyfile. * @details Copies a `NUL`-terminated string into a sized @p out_buffer. If the * buffer is too small, its content is not `NUL`-terminated. * @note This can be applied to NetworkManager connection profiles generated by Netplan, following a `netplan-ID[-SSID].nmconnection` naming scheme, as used by the NetworkManager YAML backend. * @param[in] filename Full path to a NetworkManager keyfile * @param[in] ssid Wi-Fi SSID of this connection profile, or `NULL` * @param[out] out_buffer A pre-allocated buffer to write the output string into, owned by the caller * @param[in] out_buf_size The maximum size (in bytes) available for @p out_buffer * @return The size of the copied string, including the final `NUL` character. * If the buffer is too small, returns @ref NETPLAN_BUFFER_TOO_SMALL instead. */ NETPLAN_PUBLIC ssize_t netplan_get_id_from_nm_filepath(const char* filename, const char* ssid, char* out_buffer, size_t out_buf_size); /** * @brief Free a @ref NetplanError, including any dynamically allocated data. * @details Free @p error and set `*error` to `NULL`. * @param[out] error The @ref NetplanError to free and nullify */ NETPLAN_PUBLIC void netplan_error_clear(NetplanError** error); /** * @brief Get the human readable error message of a @ref NetplanError. * @details Copies a `NUL`-terminated string into a sized @p out_buffer. If the * buffer is too small, its content is not `NUL`-terminated. * @param[in] error The @ref NetplanError to query * @param[out] buf A pre-allocated buffer to write the output string into, owned by the caller * @param[in] buf_size The maximum size (in bytes) available for @p out_buffer * @return The size of the copied string, including the final `NUL` character. * If the buffer is too small, returns @ref NETPLAN_BUFFER_TOO_SMALL instead. */ NETPLAN_PUBLIC ssize_t netplan_error_message(NetplanError* error, char* buf, size_t buf_size); /** * @brief Returns a `u64` value containing both the @ref NETPLAN_ERROR_DOMAINS * and specific error code of that domain. * @details The two values are concatenated, so relevant data can be masked: `(u32)domain | (u32)code` * * Example: * * domain = error >> 32 # upper 32 bits * code = (uint32_t) error # lower 32 bits * @note Error codes can be of enumeration type @ref NETPLAN_PARSER_ERRORS, @ref NETPLAN_VALIDATION_ERRORS, @ref NETPLAN_BACKEND_ERRORS, etc. * @return A `u64` value containing concatenated @ref NetplanError domain and a specific error code */ NETPLAN_PUBLIC uint64_t netplan_error_code(NetplanError* error); /** * @brief Create a YAML document from a `netplan-set expression`. * @details A `set expression` here consists of a path formed of `TAB`-separated * keys, indicating where in the YAML file we want to make our changes, * and a valid YAML expression that is the payload to insert at * that place. The result is a well-formed YAML document. * Example: * * # netplan set "network.ethernets.eth0={dhcp4: true}" * conf_obj_path = "network\tethernets\teth0" * obj_payload = "{dhcp4: true}" * @param[in] conf_obj_path A `TAB`-separated YAML path * @param[in] obj_payload YAML expression * @param[in,out] out_fd A file descriptor referencing the output file to contain the resulting YAML document * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_util_create_yaml_patch(const char* conf_obj_path, const char* obj_payload, int out_fd, NetplanError** error); /** * @brief Get a subset of a YAML document given in @p input_fd. * @details A `set expression` here consists of a path formed of `TAB`-separated * keys, indicating where in the YAML doc we want to make our changes, * and a valid YAML expression that is the payload to insert at * that place. The result is a well-formed YAML document. * Example: * * # netplan get "network.ethernets.eth0" * prefix = "network\tethernets\teth0" * @param[in] prefix A `TAB`-separated YAML path * @param[in] input_fd A file descriptor, referencing the input YAML file * @param[in,out] output_fd A file descriptor, referencing the output file to contain the resulting subset of the input YAML file * @param[out] error Filled with a @ref NetplanError in case of failure * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_util_dump_yaml_subtree(const char* prefix, int input_fd, int output_fd, NetplanError** error); netplan-1.0/meson.build000066400000000000000000000116711457004145200151650ustar00rootroot00000000000000project('netplan', 'c', version: '1.0', license: 'GPL3', default_options: [ 'c_std=c99', 'warning_level=2', 'werror=true', ], meson_version: '>= 0.61.0', ) glib = dependency('glib-2.0') gio = dependency('gio-2.0') yaml = dependency('yaml-0.1') uuid = dependency('uuid') libsystemd = dependency('libsystemd') meson_make_symlink = meson.current_source_dir() + '/tools/meson-make-symlink.sh' systemd = dependency('systemd') completions = dependency('bash-completion') systemd_generator_dir = systemd.get_variable(pkgconfig: 'systemdsystemgeneratordir') bash_completions_dir = completions.get_variable(pkgconfig: 'completionsdir', default_value: '/etc/bash_completion.d') # Order: Fedora/Mageia/openSUSE || Debian/Ubuntu pyflakes = find_program('pyflakes-3', 'pyflakes3', required: false) pycodestyle = find_program('pycodestyle-3', 'pycodestyle', 'pep8', required: false) pytest = find_program('pytest-3', 'pytest3') # also requires the pytest-cov plugin pycoverage = find_program('coverage-3', 'python3-coverage') pandoc = find_program('pandoc', required: false) find = find_program('find') add_project_arguments( '-DSBINDIR="' + join_paths(get_option('prefix'), get_option('sbindir')) + '"', '-D_GNU_SOURCE', language: 'c') inc = include_directories('include') inc_internal = include_directories('src') subdir('include') subdir('src') subdir('dbus') subdir('netplan_cli') subdir('python-cffi') subdir('examples') subdir('doc') pkg_mod = import('pkgconfig') pkg_mod.generate( libraries: libnetplan, subdirs: ['netplan'], name: 'libnetplan', filebase: 'netplan', description: 'YAML network configuration abstraction runtime library') install_data( 'netplan.completions', rename: 'netplan', install_dir: bash_completions_dir) ########### # Testing # ########### test_env = [ 'PYTHONPATH=' + join_paths(meson.current_build_dir(), 'python-cffi') + ':' + meson.current_source_dir(), 'LD_LIBRARY_PATH=' + join_paths(meson.current_build_dir(), 'src'), 'NETPLAN_GENERATE_PATH=' + join_paths(meson.current_build_dir(), 'src', 'generate'), 'NETPLAN_DBUS_CMD=' + join_paths(meson.current_build_dir(), 'dbus', 'netplan-dbus'), 'COVERAGE_PROCESS_START=' + join_paths(meson.current_source_dir(), '.coveragerc'), 'G_DEBUG=fatal_criticals', ] if get_option('unit_testing') subdir('tests/ctests') endif #FIXME: exclude doc/env/ test('linting', pyflakes, timeout: 100, args: [meson.current_source_dir()]) test('codestyle', pycodestyle, timeout: 100, args: ['--max-line-length=130', '--exclude=doc/env,*meson-private/pycompile.py', meson.current_source_dir()]) test('documentation', find_program('tests/validate_docs.sh'), timeout: 100, workdir: meson.current_source_dir()) test('legacy-tests', find_program('tests/cli_legacy.py'), timeout: 600, env: test_env) #TODO: split out dbus tests into own test() instance, to run in parallel test('unit-tests', pycoverage, args: ['run', '-a', '-m', 'pytest', '-s', '-v', '--cov-append', meson.current_source_dir()], timeout: 600, env: test_env) #TODO: the coverage section should probably be cleaned up a bit if get_option('b_coverage') message('Find coverage reports in /meson-logs/coveragereport[-py]/') # Using gcovr instead of lcov/gcov. # The 'ninja coverage' command will produce the html/txt reports for C implicitly #lcov = find_program('lcov') #gcov = find_program('gcov') #genhtml = find_program('genhtml') gcovr = find_program('gcovr') ninja = find_program('ninja') grep = find_program('grep') cat = find_program('cat') test('coverage-c-output', find_program('ninja'), args: ['-C', meson.current_build_dir(), 'coverage'], timeout: 60, priority: -90, # run before 'coverage-c' is_parallel: false) test('coverage-c-cat', cat, args: [join_paths(meson.current_build_dir(), 'meson-logs', 'coverage.txt')], priority: -98, # run before 'coverage-c' is_parallel: false) test('coverage-c', grep, args: ['^TOTAL.*100%$', join_paths(meson.current_build_dir(), 'meson-logs', 'coverage.txt')], priority: -99, # run last is_parallel: false) test('coverage-py-combine', pycoverage, args: ['combine', '-a', meson.current_build_dir()], priority: -90, # run before 'coverage-py-output' is_parallel: false) test('coverage-py-output', pycoverage, args: ['html', '-d', join_paths(meson.current_build_dir(), 'meson-logs', 'coveragereport-py'), '--omit=/usr/*'], priority: -95, # run before 'coverage-py' is_parallel: false) test('coverage-py', pycoverage, args: ['report', '--omit=/usr/*', '--show-missing', '--fail-under=100'], priority: -99, # run last is_parallel: false) endif netplan-1.0/meson_options.txt000066400000000000000000000000651457004145200164530ustar00rootroot00000000000000option('unit_testing', type: 'boolean', value: true) netplan-1.0/netplan.completions000066400000000000000000000054061457004145200167410ustar00rootroot00000000000000# netplan(5) completion -*- shell-script -*- _netplan_completions_filter() { local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() if [[ "${cur:0:1}" == "-" ]]; then echo "$words" else for word in $words; do [[ "${word:0:1}" != "-" ]] && result+=("$word") done echo "${result[*]}" fi } _netplan_completions() { local cur=${COMP_WORDS[COMP_CWORD]} local compwords=("${COMP_WORDS[@]:1:$COMP_CWORD-1}") local compline="${compwords[*]}" case "$compline" in 'ip leases'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug --root-dir")" -- "$cur" ) ;; 'generate'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug --root-dir --mapping")" -- "$cur" ) ;; 'rebind'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug")" -- "$cur" ) ;; 'status'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug -a --all --diff --diff-only --root-dir --verbose -f --format $(ls /sys/class/net 2> /dev/null)")" -- "$cur" ) ;; 'apply'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug --sriov-only --only-ovs-cleanup --state")" -- "$cur" ) ;; 'help'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help")" -- "$cur" ) ;; 'info'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug --json --yaml")" -- "$cur" ) ;; 'get'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug --root-dir")" -- "$cur" ) ;; 'set'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug --origin-hint")" -- "$cur" ) ;; 'try'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug --config-file --timeout --state")" -- "$cur" ) ;; 'ip'*) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug help leases")" -- "$cur" ) ;; *) while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug help apply generate get info ip set rebind status try")" -- "$cur" ) ;; esac } && complete -F _netplan_completions netplan # ex: filetype=sh netplan-1.0/netplan_cli/000077500000000000000000000000001457004145200153055ustar00rootroot00000000000000netplan-1.0/netplan_cli/__init__.py000066400000000000000000000013651457004145200174230ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .cli.core import Netplan __all__ = [Netplan] netplan-1.0/netplan_cli/cli/000077500000000000000000000000001457004145200160545ustar00rootroot00000000000000netplan-1.0/netplan_cli/cli/__init__.py000066400000000000000000000013011457004145200201600ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . netplan-1.0/netplan_cli/cli/commands/000077500000000000000000000000001457004145200176555ustar00rootroot00000000000000netplan-1.0/netplan_cli/cli/commands/__init__.py000066400000000000000000000023501457004145200217660ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .apply import NetplanApply from .generate import NetplanGenerate from .ip import NetplanIp from .migrate import NetplanMigrate from .try_command import NetplanTry from .info import NetplanInfo from .set import NetplanSet from .get import NetplanGet from .sriov_rebind import NetplanSriovRebind from .status import NetplanStatus __all__ = [ 'NetplanApply', 'NetplanGenerate', 'NetplanIp', 'NetplanMigrate', 'NetplanTry', 'NetplanInfo', 'NetplanSet', 'NetplanGet', 'NetplanSriovRebind', 'NetplanStatus', ] netplan-1.0/netplan_cli/cli/commands/apply.py000066400000000000000000000521571457004145200213660ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018-2020 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Łukasz 'sil2100' Zemczak # Author: Lukas 'slyon' Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan apply command line''' import logging import os import sys import glob import subprocess import shutil import netifaces import time from .. import utils from ...configmanager import ConfigManager, ConfigurationError from ..sriov import apply_sriov_config from ..ovs import OvsDbServerNotRunning, OvsDbServerNotInstalled, apply_ovs_cleanup OVS_CLEANUP_SERVICE = 'netplan-ovs-cleanup.service' IF_NAMESIZE = 16 class NetplanApply(utils.NetplanCommand): def __init__(self): super().__init__(command_id='apply', description='Apply current netplan config to running system', leaf=True) self.sriov_only = False self.only_ovs_cleanup = False self.state = None # to be filled by the '--state' argument def run(self): # pragma: nocover (covered in autopkgtest) self.parser.add_argument('--sriov-only', action='store_true', help='Only apply SR-IOV related configuration and exit') self.parser.add_argument('--only-ovs-cleanup', action='store_true', help='Only clean up old OpenVSwitch interfaces and exit') self.parser.add_argument('--state', help='Directory containing previous YAML configuration') self.func = self.command_apply self.parse_args() self.run_command() def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state_dir=None): # pragma: nocover config_manager = ConfigManager() if state_dir: self.state = state_dir # For certain use-cases, we might want to only apply specific configuration. # If we only need SR-IOV configuration, do that and exit early. if self.sriov_only: NetplanApply.process_sriov_config(config_manager, exit_on_error) return # If we only need OpenVSwitch cleanup, do that and exit early. elif self.only_ovs_cleanup: NetplanApply.process_ovs_cleanup(config_manager, False, False, exit_on_error) return # if we are inside a snap, then call dbus to run netplan apply instead if "SNAP" in os.environ: # TODO: maybe check if we are inside a classic snap and don't do # this if we are in a classic snap? busctl = shutil.which("busctl") if busctl is None: raise RuntimeError("missing busctl utility") # XXX: DO NOT TOUCH or change this API call, it is used by snapd to communicate # using core20 netplan binary/client/CLI on core18 base systems. Any change # must be agreed upon with the snapd team, so we don't break support for # base systems running older netplan versions. # https://github.com/snapcore/snapd/pull/5915 res = subprocess.call([busctl, "call", "--quiet", "--system", "io.netplan.Netplan", # the service "/io/netplan/Netplan", # the object "io.netplan.Netplan", # the interface "Apply", # the method ]) if res != 0: if exit_on_error: sys.exit(res) elif res == 130: raise PermissionError( "failed to communicate with dbus service") else: raise RuntimeError( "failed to communicate with dbus service: error %s" % res) else: return ovs_cleanup_service = '/run/systemd/system/netplan-ovs-cleanup.service' old_files_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) old_ovs_glob = glob.glob('/run/systemd/system/netplan-ovs-*') # Ignore netplan-ovs-cleanup.service, as it can always be there if ovs_cleanup_service in old_ovs_glob: old_ovs_glob.remove(ovs_cleanup_service) old_files_ovs = bool(old_ovs_glob) old_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*') nm_ifaces = utils.nm_interfaces(old_nm_glob, netifaces.interfaces()) old_files_nm = bool(old_nm_glob) generator_call = [] generate_out = None if 'NETPLAN_PROFILE' in os.environ: generator_call.extend(['valgrind', '--leak-check=full']) generate_out = subprocess.STDOUT generator_call.append(utils.get_generator_path()) if run_generate and subprocess.call(generator_call, stderr=generate_out) != 0: if exit_on_error: sys.exit(os.EX_CONFIG) else: raise ConfigurationError("the configuration could not be generated") devices = netifaces.interfaces() # Re-start service when # 1. We have configuration files for it # 2. Previously we had config files for it but not anymore # Ideally we should compare the content of the *netplan-* files before and # after generation to minimize the number of re-starts, but the conditions # above works too. restart_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) if not restart_networkd and old_files_networkd: restart_networkd = True restart_ovs_glob = glob.glob('/run/systemd/system/netplan-ovs-*') # Ignore netplan-ovs-cleanup.service, as it can always be there if ovs_cleanup_service in restart_ovs_glob: restart_ovs_glob.remove(ovs_cleanup_service) restart_ovs = bool(restart_ovs_glob) if not restart_ovs and old_files_ovs: # OVS is managed via systemd units restart_networkd = True restart_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*') nm_ifaces.update(utils.nm_interfaces(restart_nm_glob, devices)) restart_nm = bool(restart_nm_glob) if not restart_nm and old_files_nm: restart_nm = True # Running 'systemctl daemon-reload' will re-run the netplan systemd generator, # so let's make sure we only run it iff we're willing to run 'netplan generate' if run_generate: utils.systemctl_daemon_reload() # stop backends if restart_networkd: logging.debug('netplan generated networkd configuration changed, reloading networkd') # Clean up any old netplan related OVS ports/bonds/bridges, if applicable NetplanApply.process_ovs_cleanup(config_manager, old_files_ovs, restart_ovs, exit_on_error) wpa_services = ['netplan-wpa-*.service'] # Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an # upgraded system, we need to make sure to stop those. if utils.systemctl_is_active('netplan-wpa@*.service'): wpa_services.insert(0, 'netplan-wpa@*.service') utils.systemctl('stop', wpa_services, sync=sync) else: logging.debug('no netplan generated networkd configuration exists') loopback_connection = '' if restart_nm: logging.debug('netplan generated NM configuration changed, restarting NM') if utils.nm_running(): if 'lo' in nm_ifaces: loopback_connection = utils.nm_get_connection_for_interface('lo') # restarting NM does not cause new config to be applied, need to shut down devices first for device in devices: if device not in nm_ifaces: continue # do not touch this interface # ignore failures here -- some/many devices might not be managed by NM try: utils.nmcli(['device', 'disconnect', device]) except subprocess.CalledProcessError: pass utils.systemctl_network_manager('stop', sync=sync) else: logging.debug('no netplan generated NM configuration exists') # Refresh devices now; restarting a backend might have made something appear. devices = netifaces.interfaces() # evaluate config for extra steps we need to take (like renaming) # for now, only applies to non-virtual (real) devices. config_manager.parse() changes = NetplanApply.process_link_changes(devices, config_manager) # delete virtual interfaces that have been defined in a previous state # but are not configured anymore in the current YAML if self.state: cm = ConfigManager(self.state) cm.parse() # get previous configuration state prev_links = cm.virtual_interfaces.keys() curr_links = config_manager.virtual_interfaces.keys() NetplanApply.clear_virtual_links(prev_links, curr_links, devices) # if the interface is up, we can still apply some .link file changes # but we cannot apply the interface rename via udev, as it won't touch # the interface name, if it was already renamed once (e.g. during boot), # because of the NamePolicy=keep default: # https://www.freedesktop.org/software/systemd/man/systemd.net-naming-scheme.html devices = netifaces.interfaces() for device in devices: logging.debug('netplan triggering .link rules for %s', device) try: subprocess.check_call(['udevadm', 'test-builtin', 'net_setup_link', '/sys/class/net/' + device], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.check_call(['udevadm', 'test', '/sys/class/net/' + device], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError: logging.debug('Ignoring device without syspath: %s', device) devices_after_udev = netifaces.interfaces() # apply some more changes manually for iface, settings in changes.items(): # rename non-critical network interfaces new_name = settings.get('name') if new_name: if len(new_name) >= IF_NAMESIZE: logging.warning('Interface name {} is too long. {} will not be renamed'.format(new_name, iface)) continue if iface in devices and new_name in devices_after_udev: logging.debug('Interface rename {} -> {} already happened.'.format(iface, new_name)) continue # re-name already happened via 'udevadm test' # bring down the interface, using its current (matched) interface name subprocess.check_call(['ip', 'link', 'set', 'dev', iface, 'down'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # rename the interface to the name given via 'set-name' subprocess.check_call(['ip', 'link', 'set', 'dev', iface, 'name', settings.get('name')], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # Reloading of udev rules happens during 'netplan generate' already # subprocess.check_call(['udevadm', 'control', '--reload-rules']) subprocess.check_call(['udevadm', 'trigger', '--attr-match=subsystem=net']) subprocess.check_call(['udevadm', 'settle']) # apply any SR-IOV related changes, if applicable NetplanApply.process_sriov_config(config_manager, exit_on_error) # (re)set global regulatory domain if os.path.exists('/run/systemd/system/netplan-regdom.service'): utils.systemctl('start', ['netplan-regdom.service']) # (re)start backends if restart_networkd: netplan_wpa = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-wpa-*.service')] # exclude the special 'netplan-ovs-cleanup.service' unit netplan_ovs = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-ovs-*.service') if not f.endswith('/' + OVS_CLEANUP_SERVICE)] # Run 'systemctl start' command synchronously, to avoid race conditions # with 'oneshot' systemd service units, e.g. netplan-ovs-*.service. try: utils.networkctl_reload() utils.networkctl_reconfigure(utils.networkd_interfaces()) except subprocess.CalledProcessError: # (re-)start systemd-networkd if it is not running, yet logging.warning('Falling back to a hard restart of systemd-networkd.service') utils.systemctl('restart', ['systemd-networkd.service'], sync=True) # 1st: execute OVS cleanup, to avoid races while applying OVS config utils.systemctl('start', [OVS_CLEANUP_SERVICE], sync=True) # 2nd: start all other services utils.systemctl('start', netplan_wpa + netplan_ovs, sync=True) if restart_nm: # Flush all IP addresses of NM managed interfaces, to avoid NM creating # new, non netplan-* connection profiles, using the existing IPs. nm_interfaces = utils.nm_interfaces(restart_nm_glob, devices) for iface in nm_interfaces: utils.ip_addr_flush(iface) # clear NM state, especially the [device].managed=true config, as that might have been # re-set via an udev rule setting "NM_UNMANAGED=1" shutil.rmtree('/run/NetworkManager/devices', ignore_errors=True) utils.systemctl_network_manager('start', sync=sync) # If 'lo' is in the nm_interfaces set we flushed it's IPs (see above) and disconnected it. # NM will not bring it back automatically after restarting and we need to do that manually. # For that, we need NM up and ready to accept commands if 'lo' in nm_interfaces: sync = True if sync: # 'nmcli' could be /usr/bin/nmcli or # /snap/bin/nmcli -> /snap/bin/network-manager.nmcli cmd = ['nmcli', 'general', 'status'] # wait a bit for 'connected (site/local-only)' or # 'connected' to appear in 'nmcli general' STATE for _ in range(10): out = subprocess.run(cmd, capture_output=True, text=True) # Handle nmcli's "not running" return code (8) gracefully, # giving some more time for NetworkManager startup if out.returncode == 8: time.sleep(1) continue if '\nconnected' in str(out.stdout): break time.sleep(0.5) # If "lo" is managed by NM through Netplan, apply will flush its addresses and disconnect it. # NM will not bring it back automatically. # This is a possible scenario with netplan-everywhere. If a user tries to change the 'lo' # connection with nmcli for example, NM will create a persistent nmconnection file and emit a YAML for it. if 'lo' in nm_interfaces and loopback_connection: utils.nm_bring_interface_up(loopback_connection) @staticmethod def is_composite_member(composites, phy): """ Is this physical interface a member of a 'composite' virtual interface? (bond, bridge) """ for composite in composites: for _, settings in composite.items(): if not type(settings) is dict: continue members = settings.get('interfaces', []) for iface in members: if iface == phy: return True return False @staticmethod def clear_virtual_links(prev_links, curr_links, devices=[]): """ Calculate the delta of virtual links. And remove the links that were dropped from the YAML config, if they were not dropped by the backend already. We can make use of the netplan netdef ids, as those equal the interface name for virtual links. """ if not devices: logging.warning('Cannot clear virtual links: no network interfaces provided.') return [] dropped_interfaces = list(set(prev_links) - set(curr_links)) # some interfaces might have been cleaned up already, e.g. by the # NetworkManager backend interfaces_to_clear = list(set(dropped_interfaces).intersection(devices)) for link in interfaces_to_clear: try: cmd = ['ip', 'link', 'delete', 'dev', link] subprocess.check_call(cmd) except subprocess.CalledProcessError: logging.warning('Could not delete interface {}'.format(link)) return dropped_interfaces @staticmethod def process_link_changes(interfaces, config_manager: ConfigManager): # pragma: nocover (covered in autopkgtest) """ Go through the pending changes and pick what needs special handling. Only applies to non-critical interfaces which can be safely updated. """ changes = {} composite_interfaces = [config_manager.bridges, config_manager.bonds] # Find physical interfaces which need a rename # But do not rename virtual interfaces for netdef in config_manager.physical_interfaces.values(): newname = netdef.set_name if not newname: continue # Skip if no new name needs to be set if not netdef._has_match: continue # Skip if no match for current name is given if NetplanApply.is_composite_member(composite_interfaces, netdef.id): logging.debug('Skipping composite member {}'.format(netdef.id)) # do not rename members of virtual devices. MAC addresses # may be the same for all interface members. continue # Find current name of the interface, according to match conditions and globs (name, mac, driver) current_iface_name = utils.find_matching_iface(interfaces, netdef) if not current_iface_name: logging.warning('Cannot find unique matching interface for {}'.format(netdef.id)) continue if current_iface_name == newname: # Skip interface if it already has the correct name logging.debug('Skipping correctly named interface: {}'.format(newname)) continue if netdef.critical: # Skip interfaces defined as critical, as we should not take them down in order to rename logging.warning('Cannot rename {} ({} -> {}) at runtime (needs reboot), due to being critical' .format(netdef.id, current_iface_name, newname)) continue # record the interface rename change changes[current_iface_name] = {'name': newname} logging.debug('Link changes: {}'.format(changes)) return changes @staticmethod def process_sriov_config(config_manager, exit_on_error=True): # pragma: nocover (covered in autopkgtest) try: apply_sriov_config(config_manager) except utils.config_errors as e: logging.error(str(e)) if exit_on_error: sys.exit(1) @staticmethod def process_ovs_cleanup(config_manager, ovs_old, ovs_current, exit_on_error=True): # pragma: nocover (autopkgtest) try: apply_ovs_cleanup(config_manager, ovs_old, ovs_current) except (OSError, RuntimeError) as e: logging.error(str(e)) if exit_on_error: sys.exit(1) except OvsDbServerNotRunning as e: logging.warning('Cannot call Open vSwitch: {}.'.format(e)) except OvsDbServerNotInstalled as e: logging.debug('Cannot call Open vSwitch: %s.', e) netplan-1.0/netplan_cli/cli/commands/generate.py000066400000000000000000000067441457004145200220340ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan generate command line''' import logging import os import sys import subprocess import shutil from .. import utils class NetplanGenerate(utils.NetplanCommand): def __init__(self): super().__init__(command_id='generate', description='Generate backend specific configuration files' ' from /etc/netplan/*.yaml', leaf=True) def run(self): self.parser.add_argument('--root-dir', help='Search for and generate configuration files in this root directory instead of /') self.parser.add_argument('--mapping', help='Display the netplan device ID/backend/interface name mapping and exit.') self.func = self.command_generate self.parse_args() self.run_command() def command_generate(self): # if we are inside a snap, then call dbus to run netplan apply instead if "SNAP" in os.environ: # TODO: maybe check if we are inside a classic snap and don't do # this if we are in a classic snap? busctl = shutil.which("busctl") if busctl is None: raise RuntimeError("missing busctl utility") # pragma: nocover # XXX: DO NOT TOUCH or change this API call, it is used by snapd to communicate # using core20 netplan binary/client/CLI on core18 base systems. Any change # must be agreed upon with the snapd team, so we don't break support for # base systems running older netplan versions. # https://github.com/snapcore/snapd/pull/10212 res = subprocess.call([busctl, "call", "--quiet", "--system", "io.netplan.Netplan", # the service "/io/netplan/Netplan", # the object "io.netplan.Netplan", # the interface "Generate", # the method ]) if res != 0: if res == 130: raise PermissionError( "PermissionError: failed to communicate with dbus service") else: raise RuntimeError( "RuntimeError: failed to communicate with dbus service: error %s" % res) else: return argv = [utils.get_generator_path()] if self.root_dir: argv += ['--root-dir', self.root_dir] if self.mapping: argv += ['--mapping', self.mapping] logging.debug('command generate: running %s', argv) # FIXME: os.execv(argv[0], argv) would be better but fails coverage sys.exit(subprocess.call(argv)) netplan-1.0/netplan_cli/cli/commands/get.py000066400000000000000000000030301457004145200210020ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2020-2023 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan get command line''' from ..state import NetplanConfigState from .. import utils class NetplanGet(utils.NetplanCommand): def __init__(self): super().__init__(command_id='get', description='Get a setting by specifying a nested key like "ethernets.eth0.addresses", or "all"', leaf=True) def run(self): self.parser.add_argument('key', type=str, nargs='?', default='all', help='The nested key in dotted format') self.parser.add_argument('--root-dir', default='/', help='Read configuration files from this root directory instead of /') self.func = self.command_get self.parse_args() self.run_command() def command_get(self): state_data = NetplanConfigState(self.key, self.root_dir) print(state_data, end='') netplan-1.0/netplan_cli/cli/commands/info.py000066400000000000000000000046361457004145200211730ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2019 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan info command line''' from .. import utils from ... import _features class NetplanInfo(utils.NetplanCommand): def __init__(self): super().__init__(command_id='info', description='Show available features', leaf=True) def run(self): # pragma: nocover (covered in autopkgtest) format_group = self.parser.add_mutually_exclusive_group(required=False) format_group.add_argument('--json', dest='version_format', action='store_const', const='json', help='Output version and features in JSON format') format_group.add_argument('--yaml', dest='version_format', action='store_const', const='yaml', help='Output version and features in YAML format') self.func = self.command_info self.parse_args() self.run_command() def command_info(self): netplan_version = { 'netplan.io': { 'website': 'https://netplan.io/', } } flags = _features.NETPLAN_FEATURE_FLAGS netplan_version['netplan.io'].update({'features': flags}) # Default to output in YAML format. if self.version_format is None: self.version_format = 'yaml' if self.version_format == 'json': import json print(json.dumps(netplan_version, indent=2)) elif self.version_format == 'yaml': print('''netplan.io: website: "{}" features:'''.format(netplan_version['netplan.io']['website'])) for feature in _features.NETPLAN_FEATURE_FLAGS: print(' - ' + feature) netplan-1.0/netplan_cli/cli/commands/ip.py000066400000000000000000000141261457004145200206430ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan ip command line''' import logging import os import sys import subprocess from subprocess import CalledProcessError from .. import utils lease_path = { 'networkd': { 'pattern': 'run/systemd/netif/leases/{lease_id}', 'method': 'ifindex', }, 'NetworkManager': { 'pattern': 'var/lib/NetworkManager/internal-{lease_id}-{interface}.lease', 'method': 'nm_connection', }, } class NetplanIp(utils.NetplanCommand): def __init__(self): super().__init__(command_id='ip', description='Retrieve IP information from the system', leaf=False) def run(self): self.command_leases = NetplanIpLeases() # subcommand: leases p_ip_leases = self.subparsers.add_parser('leases', help='Display IP leases', add_help=False) p_ip_leases.set_defaults(func=self.command_leases.run, commandclass=self.command_leases) self.parse_args() self.run_command() class NetplanIpLeases(utils.NetplanCommand): def __init__(self): super().__init__(command_id='ip leases', description='Display IP leases', leaf=True) def run(self): self.parser.add_argument('interface', help='Interface for which to display IP lease settings.') self.parser.add_argument('--root-dir', help='Search for configuration files in this root directory instead of /') self.func = self.command_ip_leases self.parse_args() self.run_command() def command_ip_leases(self): if self.interface == 'help': # pragma: nocover (covered in autopkgtest) self.print_usage() def find_lease_file(mapping): def lease_method_ifindex(): ifindex_f = os.path.join('/sys/class/net', self.interface, 'ifindex') try: with open(ifindex_f) as f: return f.readlines()[0].strip() except Exception as e: logging.debug('Cannot read file %s: %s', ifindex_f, str(e)) raise def lease_method_nm_connection(): # FIXME: handle older versions of NM where 'nmcli dev show' doesn't exist try: nmcli_dev_out = utils.nmcli_out(['dev', 'show', self.interface]) for line in nmcli_dev_out.splitlines(): if 'GENERAL.CONNECTION' in line: conn_id = line.split(':')[1].rstrip().strip() nmcli_con_out = utils.nmcli_out(['con', 'show', 'id', conn_id]) for line in nmcli_con_out.splitlines(): if 'connection.uuid' in line: return line.split(':')[1].rstrip().strip() except Exception as e: raise Exception('Could not find a NetworkManager connection for the interface: %s' % str(e)) raise Exception('Could not find a NetworkManager connection for the interface') lease_pattern = lease_path[mapping['backend']]['pattern'] lease_method = lease_path[mapping['backend']]['method'] try: lease_id = eval("lease_method_" + lease_method)() # We found something to build the path to the lease file with, # at this point we may have something to look at; but if not, # we'll rely on open() throwing an error. # This might happen if networkd doesn't use DHCP for the interface, # for instance. path = os.path.join('/', os.path.abspath(self.root_dir) if self.root_dir else "", lease_pattern.format(interface=self.interface, lease_id=lease_id)) # Fallback to 'dhclient' if no lease of NetworkManager's # internal DHCP client is found if not os.path.isfile(path): path = path.replace('NetworkManager/internal-', 'NetworkManager/dhclient-') with open(path) as f: for line in f.readlines(): print(line.rstrip()) except Exception as e: print("No lease found for interface '%s': %s" % (self.interface, str(e)), file=sys.stderr) sys.exit(1) argv = [utils.get_generator_path()] if self.root_dir: argv += ['--root-dir', self.root_dir] argv += ['--mapping', self.interface] # Extract out of the generator our mapping in a dict. logging.debug('command ip leases: running %s', argv) try: out = subprocess.check_output(argv, text=True) except CalledProcessError: # pragma: nocover (better be covered in autopkgtest) print("No lease found for interface '%s' (not managed by Netplan)" % self.interface, file=sys.stderr) sys.exit(1) mapping = {} mapping_s = out.split(',') for keyvalue in mapping_s: key, value = keyvalue.strip().split('=') mapping[key] = value find_lease_file(mapping) netplan-1.0/netplan_cli/cli/commands/migrate.py000066400000000000000000000477461457004145200217010ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan migrate command line''' import logging import os import sys import re from glob import glob try: import yaml NO_YAML = False except ImportError: # pragma: nocover NO_YAML = True from collections import OrderedDict import ipaddress from .. import utils class NetplanMigrate(utils.NetplanCommand): def __init__(self): super().__init__(command_id='migrate', description='Migration of /etc/network/interfaces to netplan', leaf=True, testing=True) def parse_dns_options(self, if_options, if_config): """Parse dns options (dns-nameservers and dns-search) from if_options (an interface options dict) into the interface configuration if_config Mutates the arguments in place. """ if 'dns-nameservers' in if_options: if 'nameservers' not in if_config: if_config['nameservers'] = {} if 'addresses' not in if_config['nameservers']: if_config['nameservers']['addresses'] = [] for ns in if_options['dns-nameservers'].split(' '): # allow multiple spaces in the dns-nameservers entry if not ns: continue # validate? if_config['nameservers']['addresses'] += [ns] del if_options['dns-nameservers'] if 'dns-search' in if_options: if 'nameservers' not in if_config: if_config['nameservers'] = {} if 'search' not in if_config['nameservers']: if_config['nameservers']['search'] = [] for domain in if_options['dns-search'].split(' '): # allow multiple spaces in the dns-search entry if not domain: continue if_config['nameservers']['search'] += [domain] del if_options['dns-search'] def parse_mtu(self, iface, if_options, if_config): """Parse out the MTU. Operates the same way as parse_dns_options iface is the name of the interface, used only to print error messages """ if 'mtu' in if_options: try: mtu = int(if_options['mtu']) except ValueError: logging.error('%s: cannot parse "%s" as an MTU', iface, if_options['mtu']) sys.exit(2) if 'mtu' in if_config and not if_config['mtu'] == mtu: logging.error('%s: tried to set MTU=%d, but already have MTU=%d', iface, mtu, if_config['mtu']) sys.exit(2) if_config['mtu'] = mtu del if_options['mtu'] def parse_hwaddress(self, iface, if_options, if_config): """Parse out the manually configured MAC. Operates the same way as parse_dns_options iface is the name of the interface, used only to print error messages """ if 'hwaddress' in if_options: if 'macaddress' in if_config and not if_config['macaddress'] == if_options['hwaddress']: logging.error('%s: tried to set MAC %s, but already have MAC %s', iface, if_options['hwaddress'], if_config['macaddress']) sys.exit(2) if_config['macaddress'] = if_options['hwaddress'] del if_options['hwaddress'] def run(self): self.parser.add_argument('--root-dir', help='Search for and generate configuration files in this root directory instead of /') self.parser.add_argument('--dry-run', action='store_true', help='Print converted netplan configuration to stdout instead of writing/changing files') self.func = self.command_migrate self.parse_args() if NO_YAML: # pragma: nocover logging.error("""The `yaml` Python package couldn't be imported, and is needed for the migrate command. To install it on Debian or Ubuntu-based system, run `apt install python3-yaml`""") sys.exit(1) self.run_command() def command_migrate(self): netplan_config = {} try: ifaces, auto_ifaces = self.parse_ifupdown(self.root_dir or '') except ValueError as e: logging.error(str(e)) sys.exit(2) for iface, family_config in ifaces.items(): for family, config in family_config.items(): logging.debug('Converting %s family %s %s', iface, family, config) if iface not in auto_ifaces: logging.error('%s: non-automatic interfaces are not supported', iface) sys.exit(2) if config['method'] == 'loopback': # both systemd and modern ifupdown set up lo automatically logging.debug('Ignoring loopback interface %s', iface) elif config['method'] == 'dhcp': c = netplan_config.setdefault('network', {}).setdefault('ethernets', {}).setdefault(iface, {}) self.parse_dns_options(config['options'], c) self.parse_hwaddress(iface, config['options'], c) if config['options']: logging.error('%s: option(s) %s are not supported for dhcp method', iface, ", ".join(config['options'].keys())) sys.exit(2) if family == 'inet': c['dhcp4'] = True else: assert family == 'inet6' c['dhcp6'] = True elif config['method'] == 'static': c = netplan_config.setdefault('network', {}).setdefault('ethernets', {}).setdefault(iface, {}) if 'addresses' not in c: c['addresses'] = [] self.parse_dns_options(config['options'], c) self.parse_mtu(iface, config['options'], c) self.parse_hwaddress(iface, config['options'], c) # ipv4 if family == 'inet': # Already handled: mtu, hwaddress # Supported: address netmask gateway # Not supported yet: metric(?) # No YAML support: pointopoint scope broadcast supported_opts = set(['address', 'netmask', 'gateway']) unsupported_opts = set(['broadcast', 'metric', 'pointopoint', 'scope']) opts = set(config['options'].keys()) bad_opts = opts - supported_opts if bad_opts: for unsupported in bad_opts.intersection(unsupported_opts): logging.error('%s: unsupported %s option "%s"', iface, family, unsupported) sys.exit(2) for unknown in bad_opts - unsupported_opts: logging.error('%s: unknown %s option "%s"', iface, family, unknown) sys.exit(2) # the address may contain a /prefix suffix, or # the netmask property may be used. It's not clear # what happens if both are supplied. if 'address' not in config['options']: logging.error('%s: no address supplied in static method', iface) sys.exit(2) if '/' in config['options']['address']: addr_spec = config['options']['address'].split('/')[0] net_spec = config['options']['address'] else: if 'netmask' not in config['options']: logging.error('%s: address does not specify prefix length, and netmask not specified', iface) sys.exit(2) addr_spec = config['options']['address'] net_spec = config['options']['address'] + '/' + config['options']['netmask'] try: ipaddr = ipaddress.IPv4Address(addr_spec) except ipaddress.AddressValueError as a: logging.error('%s: error parsing "%s" as an IPv4 address: %s', iface, addr_spec, a) sys.exit(2) try: ipnet = ipaddress.IPv4Network(net_spec, strict=False) except ipaddress.NetmaskValueError as a: logging.error('%s: error parsing "%s" as an IPv4 network: %s', iface, net_spec, a) sys.exit(2) c['addresses'] += [str(ipaddr) + '/' + str(ipnet.prefixlen)] if 'gateway' in config['options']: # validate? c['gateway4'] = config['options']['gateway'] # ipv6 else: assert family == 'inet6' # Already handled: mtu, hwaddress # supported: address netmask gateway # partially supported: accept_ra (0/1 supported, 2 has no YAML rep) # unsupported: metric(?) # no YAML representation: media autoconf privext scope # preferred-lifetime dad-attempts dad-interval supported_opts = set(['address', 'netmask', 'gateway', 'accept_ra']) unsupported_opts = set(['metric', 'media', 'autoconf', 'privext', 'scope', 'preferred-lifetime', 'dad-attempts', 'dad-interval']) opts = set(config['options'].keys()) bad_opts = opts - supported_opts if bad_opts: for unsupported in bad_opts.intersection(unsupported_opts): logging.error('%s: unsupported %s option "%s"', iface, family, unsupported) sys.exit(2) for unknown in bad_opts - unsupported_opts: logging.error('%s: unknown %s option "%s"', iface, family, unknown) sys.exit(2) # the address may contain a /prefix suffix, or # the netmask property may be used. It's not clear # what happens if both are supplied. if 'address' not in config['options']: logging.error('%s: no address supplied in static method', iface) sys.exit(2) if '/' in config['options']['address']: addr_spec = config['options']['address'].split('/')[0] net_spec = config['options']['address'] else: if 'netmask' not in config['options']: logging.error('%s: address does not specify prefix length, and netmask not specified', iface) sys.exit(2) addr_spec = config['options']['address'] net_spec = config['options']['address'] + '/' + config['options']['netmask'] try: ipaddr = ipaddress.IPv6Address(addr_spec) except ipaddress.AddressValueError as a: logging.error('%s: error parsing "%s" as an IPv6 address: %s', iface, addr_spec, a) sys.exit(2) try: ipnet = ipaddress.IPv6Network(net_spec, strict=False) except ipaddress.NetmaskValueError as a: logging.error('%s: error parsing "%s" as an IPv6 network: %s', iface, net_spec, a) sys.exit(2) c['addresses'] += [str(ipaddr) + '/' + str(ipnet.prefixlen)] if 'gateway' in config['options']: # validate? c['gateway6'] = config['options']['gateway'] if 'accept_ra' in config['options']: if config['options']['accept_ra'] == '0': c['accept_ra'] = False elif config['options']['accept_ra'] == '1': c['accept_ra'] = True elif config['options']['accept_ra'] == '2': logging.error('%s: netplan does not support accept_ra=2', iface) sys.exit(2) else: logging.error('%s: unexpected accept_ra value "%s"', iface, config['options']['accept_ra']) sys.exit(2) else: # pragma nocover # this should be unreachable logging.error('%s: method %s is not supported', iface, config['method']) sys.exit(2) if_config = os.path.join(self.root_dir or '/', 'etc/network/interfaces') if netplan_config: netplan_config['network']['version'] = 2 netplan_yaml = yaml.dump(netplan_config) if self.dry_run: print(netplan_yaml) else: dest = os.path.join(self.root_dir or '/', 'etc/netplan/10-ifupdown.yaml') try: os.makedirs(os.path.dirname(dest)) except FileExistsError: pass try: with open(dest, 'x') as f: f.write(netplan_yaml) except FileExistsError: logging.error('%s already exists; remove it if you want to run the migration again', dest) sys.exit(3) logging.info('migration complete, wrote %s', dest) else: logging.info('ifupdown does not configure any interfaces, nothing to migrate') if not self.dry_run: logging.info('renaming %s to %s.netplan-converted', if_config, if_config) os.rename(if_config, if_config + '.netplan-converted') def _ifupdown_lines_from_file(self, rootdir, path): '''Return normalized lines from ifupdown config This resolves "source" and "source-directory" includes. ''' def expand_source_arg(rootdir, curdir, line): arg = line.split()[1] if arg.startswith('/'): return rootdir + arg else: return curdir + '/' + arg lines = [] rootdir_len = len(rootdir) + 1 try: with open(rootdir + '/' + path) as f: logging.debug('reading %s', f.name) for line in f: # normalize, strip empty lines and comments line = line.strip() if not line or line.startswith('#'): continue if line.startswith('source-directory '): valid_re = re.compile('^[a-zA-Z0-9_-]+$') d = expand_source_arg(rootdir, os.path.dirname(f.name), line) for f in os.listdir(d): if valid_re.match(f): lines += self._ifupdown_lines_from_file(rootdir, os.path.join(d[rootdir_len:], f)) elif line.startswith('source '): for f in glob(expand_source_arg(rootdir, os.path.dirname(f.name), line)): lines += self._ifupdown_lines_from_file(rootdir, f[rootdir_len:]) else: lines.append(line) except FileNotFoundError: logging.debug('%s/%s does not exist, ignoring', rootdir, path) return lines def parse_ifupdown(self, rootdir='/'): '''Parse ifupdown configuration. Return (iface_name → family → {method, options}, auto_ifaces: set) tuple on successful parsing, or a ValueError when encountering an invalid file or ifupdown features which are not supported (such as "mapping"). options is itself a dictionary option_name → value. ''' # expected number of fields for every possible keyword, excluding the keyword itself fieldlen = {'auto': 1, 'allow-auto': 1, 'allow-hotplug': 1, 'mapping': 1, 'no-scripts': 1, 'iface': 3} # read and normalize all lines from config, with resolving includes lines = self._ifupdown_lines_from_file(rootdir, '/etc/network/interfaces') ifaces = OrderedDict() auto = set() in_options = None # interface name if parsing options lines after iface stanza in_family = None # we now have resolved all includes and normalized lines for line in lines: fields = line.split() try: # does the line start with a known stanza field? exp_len = fieldlen[fields[0]] logging.debug('line fields %s (expected length: %i)', fields, exp_len) in_options = None # stop option line parsing of iface stanza in_family = None except KeyError: # no known stanza field, are we in an iface stanza and parsing options? if in_options: logging.debug('in_options %s, parsing as option: %s', in_options, line) ifaces[in_options][in_family]['options'][fields[0]] = line.split(maxsplit=1)[1] continue else: raise ValueError('Unknown stanza type %s' % fields[0]) # do we have the expected #parameters? if len(fields) != exp_len + 1: raise ValueError('Expected %i fields for stanza type %s but got %i' % (exp_len, fields[0], len(fields) - 1)) # we have a valid stanza line now, handle them if fields[0] in ('auto', 'allow-auto', 'allow-hotplug'): auto.add(fields[1]) elif fields[0] == 'mapping': raise ValueError('mapping stanza is not supported') elif fields[0] == 'no-scripts': pass # ignore these elif fields[0] == 'iface': if fields[2] not in ('inet', 'inet6'): raise ValueError('Unknown address family %s' % fields[2]) if fields[3] not in ('loopback', 'static', 'dhcp'): raise ValueError('Unsupported method %s' % fields[3]) in_options = fields[1] in_family = fields[2] ifaces.setdefault(fields[1], OrderedDict())[in_family] = {'method': fields[3], 'options': {}} else: raise NotImplementedError('stanza type %s is not implemented' % fields[0]) # pragma nocover logging.debug('final parsed interfaces: %s; auto ifaces: %s', ifaces, auto) return (ifaces, auto) netplan-1.0/netplan_cli/cli/commands/set.py000066400000000000000000000131121457004145200210200ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2020-2023 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan set command line''' import tempfile import re import io from ..utils import NetplanCommand import netplan FALLBACK_FILENAME = '70-netplan-set.yaml' GLOBAL_KEYS = ['renderer', 'version'] class NetplanSet(NetplanCommand): def __init__(self): super().__init__(command_id='set', description='Add new setting by specifying a dotted key=value pair like ethernets.eth0.dhcp4=true', leaf=True) def run(self): self.parser.add_argument('key_value', type=str, help='The nested key=value pair in dotted format. Value can be NULL to delete a key.') self.parser.add_argument('--origin-hint', type=str, help='Can be used to help choose a name for the overwrite YAML file. \ A .yaml suffix will be appended automatically.') self.parser.add_argument('--root-dir', default='/', help='Overwrite configuration files in this root directory instead of /') self.func = self.command_set self.parse_args() self.run_command() def command_set(self): if self.origin_hint is not None and len(self.origin_hint) == 0: raise Exception('Invalid/empty origin-hint') if self.origin_hint: filename = '.'.join((self.origin_hint, 'yaml')) else: filename = None split = self.key_value.split('=', 1) if len(split) != 2: raise Exception('Invalid value specified') key, value = split if not key.startswith('network'): key = '.'.join(('network', key)) # Split the string into a list on the dot separators, and unescape the remaining dots yaml_path = [s.replace(r'\.', '.') for s in re.split(r'(?), have they been defined in # pre-existing YAML files or not. tmp.seek(0, io.SEEK_SET) parser_output_file._load_nullable_overrides(tmp, constraint=filename) # Parse the full YAML hierarchy and new patch, ignoring any # nullable overrides (netdefs/globals) from pre-existing files # and ignoring any nullable fields (settings to be deleted). # This way we can avoid updates to certain netdefs/globals to be # redirected into existing YAML files (defining those same # stanzas) or ignored, but have them written out to the single # output file. # XXX: The origin file of each individual YAML setting/stanza # should be tracked individually, to avoid this # double-parsing workaround (LP: #2003727) parser_output_file.load_yaml_hierarchy(self.root_dir) tmp.seek(0, io.SEEK_SET) parser_output_file.load_yaml(tmp) # Import the partial parser state, ignoring duplicated netdefs # from pre-existing YAML files, so we can force write the patch # contents to the output file or update this file if exists. state_output_file = netplan.State() state_output_file.import_parser_results(parser_output_file) state_output_file._write_yaml_file(filename, self.root_dir) else: state._update_yaml_hierarchy(FALLBACK_FILENAME, self.root_dir) netplan-1.0/netplan_cli/cli/commands/sriov_rebind.py000066400000000000000000000173621457004145200227250ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2022 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan SR-IOV rebind command line''' import logging import os import sys from time import sleep from .. import utils from ..sriov import PCIDevice, bind_vfs, _get_pci_slot_name import netplan FALLBACK_WAIT_TIME_SEC = 3 INTERVAL_SEC = 0.2 MAX_WAITING_TIME_SEC = 5 class MLX5VFLAGStateNotFound(Exception): pass class MLX5VFLAGStateCannotBeRead(Exception): pass class MLX5VFLAGStateDisabled(Exception): pass class NetplanSriovRebind(utils.NetplanCommand): def __init__(self): super().__init__(command_id='rebind', description='Rebind SR-IOV virtual functions of given physical functions to their driver', leaf=True) def run(self): self.parser.add_argument('--root-dir', default='/', help='Search for configuration files in this root directory instead of /') self.parser.add_argument('netdevs', type=str, nargs='*', default=[], help='Space separated list of PF interface names') self.func = self.command_rebind self.logger = logging.getLogger('sriov_rebind') self.logger.propagate = False log_handler = logging.StreamHandler(stream=sys.stdout) self.parse_args() # netplan rebind --debug setup if self.debug: self.logger.setLevel(logging.DEBUG) log_handler.setLevel(logging.DEBUG) log_handler.setFormatter(logging.Formatter('%(levelname)s:%(message)s')) else: self.logger.setLevel(logging.INFO) log_handler.setLevel(logging.INFO) log_handler.setFormatter(logging.Formatter('%(message)s')) self.logger.addHandler(log_handler) self.run_command() def command_rebind(self): """Bind virtual functions of SR-IOV devices to their corresponding driver after eswitch mode was changed""" for iface in self.netdevs: pci_addr = _get_pci_slot_name(iface) pcidev = PCIDevice(pci_addr) if not pcidev.is_pf: self.logger.debug('{} does not seem to be a SR-IOV physical function'.format(iface)) continue # There are some hardware-specific configuration that must happen *before* the bind # of VFs to their drivers. Some settings take time to be effective and, when possible, # we need to wait until the driver reports it's ready. self._perform_hardware_specific_quirks(iface, pcidev) bound_vfs = bind_vfs(pcidev.vfs, pcidev.driver) self.logger.debug('{}: bound {} VFs'.format(pcidev, len(bound_vfs))) def _perform_hardware_specific_quirks(self, iface: str, pf: PCIDevice): """ Perform any hardware-specific quirks for the given SR-IOV device to make sure it's ready before the bind. """ if pf.driver in ['mlx5_core']: # Mellanox specific quirks parser = netplan.Parser() parser.load_yaml_hierarchy(self.root_dir) np_state = netplan.State() np_state.import_parser_results(parser) for netdef in np_state.ethernets.values(): if (netdef._has_match and netdef.set_name == iface) or netdef.id == iface: if bond_link := netdef.links.get('bond'): # VF LAG support. See LP: #1988018 # If the PF is a member of a bond, the user might be trying to enable the # VF LAG feature. # Mellanox VF LAG requires that the LAG state reports as 'active' # *before* VFs can be bound to the driver. Performing the bind operation # before the device is ready will cause the VF LAG feature to never be enabled. # Another condition for the VF LAG activation is that the LAG mode # must be one of 'active-backup', 'balanced-xor' or '802.3ad'. bond_mode = bond_link._bond_mode if not self._is_bond_mode_supported(bond_mode): self.logger.debug(f'{iface} - LAG mode {bond_mode} is not supported by VF LAG') continue self.logger.debug(f'{iface} - waiting for the LAG state to be \'active\'') try: self._wait_for_mlx5_pf_lag_state_active(pf) except MLX5VFLAGStateCannotBeRead: self.logger.debug(f'{iface} - VF LAG state cannot be read') except MLX5VFLAGStateNotFound: self.logger.debug(f'{iface} - VF LAG state debugfs file not found') except MLX5VFLAGStateDisabled: self.logger.debug(f'{iface} - VF LAG state is still \'disabled\' after waiting') else: self.logger.debug(f'{iface} - VF LAG state is \'active\'') def _wait_for_mlx5_pf_lag_state_active(self, pf: PCIDevice): """ The mlx5 driver added support for debugfs in https://github.com/torvalds/linux/commit/7f46a0b7327a It's available since kernel 5.19 https://cdn.kernel.org/pub/linux/kernel/v5.x/ChangeLog-5.19 """ retries = int(MAX_WAITING_TIME_SEC / INTERVAL_SEC) pci_addr = pf.pci_addr path = f'/sys/kernel/debug/mlx5/{pci_addr}/lag/state' if not os.path.exists(path): # If the debugfs file doesn't exist, it might be because this version of the mlx5 driver # still doesn't support it or because the debugfs is not mounted. # In this case, we probably should still wait for a few seconds to give time for the # driver to change state. # Based on tests with a ConnectX-5 NIC, 1 second is enough time, so let's wait a bit more # just in case. This delay will only be introduced if the PF is part of a bond. sleep(FALLBACK_WAIT_TIME_SEC) raise MLX5VFLAGStateNotFound while retries > 0: try: if self._get_mlx5_vf_lag_state(pci_addr) != 'active': self.logger.debug(f'{pci_addr} VF LAG state is not active yet, retrying...') # Based on tests with a ConnectX-5 NIC, a single 1-second cycle was enough time to # allow the interfaces to change state. sleep(INTERVAL_SEC) else: return except Exception: raise MLX5VFLAGStateCannotBeRead retries = retries - 1 raise MLX5VFLAGStateDisabled def _is_bond_mode_supported(self, mode: str) -> bool: ''' Return True or False if the bond mode is one of the supported modes for the VG LAG activation. ''' return mode in ['active-backup', 'balanced-xor', '802.3ad'] def _get_mlx5_vf_lag_state(self, pci_addr: str) -> str: path = f'/sys/kernel/debug/mlx5/{pci_addr}/lag/state' with open(path, 'r') as f: return f.read().strip() netplan-1.0/netplan_cli/cli/commands/status.py000066400000000000000000001106011457004145200215510ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2022 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan status command line''' import json import logging import re from netplan.netdef import NetplanRoute import yaml from .. import utils from ..state import NetplanConfigState, SystemConfigState, JSON from ..state_diff import DiffJSONEncoder, NetplanDiffState MATCH_TAGS = re.compile(r'\[([a-z0-9]+)\].*\[\/\1\]') RICH_OUTPUT = False try: from rich.console import Console from rich.highlighter import RegexHighlighter from rich.theme import Theme class NetplanHighlighter(RegexHighlighter): base_style = 'netplan.' highlights = [ r'(^|[\s\/])(?P\d+)([\s:]?\s|$)', r'(?P(\"|\').+(\"|\'))', ] RICH_OUTPUT = True except ImportError: # pragma: nocover (we mock RICH_OUTPUT, ignore the logging) logging.debug("python3-rich not found, falling back to plain output") class NetplanStatus(utils.NetplanCommand): def __init__(self): super().__init__(command_id='status', description='Query networking state of the running system', leaf=True) self.all = False self.state_diff = None self.route_lookup_table_names = {} def run(self): self.parser.add_argument('ifname', nargs='?', type=str, default=None, help='Show only this interface') self.parser.add_argument('-a', '--all', action='store_true', help='Show all interface data (incl. inactive)') self.parser.add_argument('-v', '--verbose', action='store_true', help='Show extra information') self.parser.add_argument('-f', '--format', default='tabular', help='Output in machine readable `json` or `yaml` format') self.parser.add_argument('--diff', action='store_true', help='Show the differences between the system\'s and netplan\'s states') self.parser.add_argument('--diff-only', action='store_true', help='Only show the differences between the system\'s and netplan\'s states') self.parser.add_argument('--root-dir', help='Search for configuration files in this root directory instead of /') self.func = self.command self.parse_args() self.run_command() def _create_pretty_print(self, _console_width): if RICH_OUTPUT: # TODO: Use a proper (subiquity?) color palette theme = Theme({ 'netplan.int': 'bold cyan', 'netplan.str': 'yellow', 'muted': 'grey62', 'online': 'green bold', 'offline': 'red bold', 'unknown': 'yellow bold', 'highlight': 'bold' }) if self.diff: theme = Theme({ 'netplan.int': 'grey62', 'netplan.str': 'grey62', 'muted': 'grey62', 'online': 'green bold', 'offline': 'red bold', 'unknown': 'yellow bold', 'highlight': 'bold' }) console = Console(highlighter=NetplanHighlighter(), theme=theme, width=_console_width, emoji=False) pprint = console.print else: pprint = self.plain_print return pprint def _get_interface_diff(self, ifname) -> dict: if self.state_diff: if diff := self.state_diff['interfaces'].get(ifname): if diff.get('system_state') or diff.get('netplan_state'): return diff return {} def _is_interface_missing_in_netplan(self, ifname) -> bool: if self.state_diff: if missing := self.state_diff.get('missing_interfaces_netplan'): if ifname in missing: return True return False def _get_missing_property_list(self, ifname: str, state: str, property: str) -> list[str]: if self.state_diff: if diff := self.state_diff['interfaces'].get(ifname): return diff.get(state, {}).get(property, []) return [] def _get_missing_property_str(self, ifname: str, state: str, property: str) -> str: if self.state_diff: if diff := self.state_diff['interfaces'].get(ifname): return diff.get(state, {}).get(property, '') return '' def _get_missing_property_set(self, ifname: str, state: str, property: str) -> set: if self.state_diff: if diff := self.state_diff['interfaces'].get(ifname): return diff.get(state, {}).get(property, set()) return set() def _get_missing_property_bool(self, ifname: str, state: str, property: str) -> bool: if self.state_diff: if diff := self.state_diff['interfaces'].get(ifname): return diff.get(state, {}).get(property, False) return False def _get_missing_netplan_addresses(self, ifname) -> list[str]: return self._get_missing_property_list(ifname, 'netplan_state', 'missing_addresses') def _get_missing_system_nameservers(self, ifname) -> list[str]: return self._get_missing_property_list(ifname, 'system_state', 'missing_nameservers_addresses') def _get_missing_netplan_nameservers(self, ifname) -> list[str]: return self._get_missing_property_list(ifname, 'netplan_state', 'missing_nameservers_addresses') def _get_missing_netplan_search(self, ifname) -> list[str]: return self._get_missing_property_list(ifname, 'netplan_state', 'missing_nameservers_search') def _get_missing_system_search(self, ifname) -> list[str]: return self._get_missing_property_list(ifname, 'system_state', 'missing_nameservers_search') def _get_missing_system_macaddress(self, ifname) -> str: return self._get_missing_property_str(ifname, 'system_state', 'missing_macaddress') def _get_missing_netplan_routes(self, ifname) -> set[NetplanRoute]: return self._get_missing_property_set(ifname, 'netplan_state', 'missing_routes') def _get_missing_system_routes(self, ifname) -> set[NetplanRoute]: return self._get_missing_property_set(ifname, 'system_state', 'missing_routes') def _is_missing_dhcp4_address(self, ifname) -> bool: return self._get_missing_property_bool(ifname, 'system_state', 'missing_dhcp4_address') def _is_missing_dhcp6_address(self, ifname) -> bool: return self._get_missing_property_bool(ifname, 'system_state', 'missing_dhcp6_address') def _get_missing_system_bond_link(self, ifname) -> str: return self._get_missing_property_str(ifname, 'system_state', 'missing_bond_link') def _get_missing_netplan_bond_link(self, ifname) -> str: return self._get_missing_property_str(ifname, 'netplan_state', 'missing_bond_link') def _get_missing_system_bridge_link(self, ifname) -> str: return self._get_missing_property_str(ifname, 'system_state', 'missing_bridge_link') def _get_missing_netplan_bridge_link(self, ifname) -> str: return self._get_missing_property_str(ifname, 'netplan_state', 'missing_bridge_link') def _get_missing_system_vrf_link(self, ifname) -> str: return self._get_missing_property_str(ifname, 'system_state', 'missing_vrf_link') def _get_missing_netplan_vrf_link(self, ifname) -> str: return self._get_missing_property_str(ifname, 'netplan_state', 'missing_vrf_link') def _get_missing_netplan_members(self, ifname) -> list[str]: return self._get_missing_property_list(ifname, 'netplan_state', 'missing_interfaces') def _get_missing_system_members(self, ifname) -> list[str]: return self._get_missing_property_list(ifname, 'system_state', 'missing_interfaces') def _get_missing_system_interfaces(self) -> dict: if self.state_diff: return self.state_diff.get('missing_interfaces_system', {}) return {} def _has_diff(self, ifname) -> bool: if self._is_interface_missing_in_netplan(ifname): return True if self.state_diff: if diff := self.state_diff['interfaces'].get(ifname): if diff.get('system_state') or diff.get('netplan_state'): return True return False def _display_global_state(self, data): global_state = data.get('netplan-global-state', {}) self.pprint(('{title:>'+self.PAD+'} {value}').format( title='Online state:', value='[online]online[/online]' if global_state.get('online', False) else '[offline]offline[/offline]', )) ns = global_state.get('nameservers', {}) dns_addr: list = ns.get('addresses', []) dns_mode: str = ns.get('mode') dns_search: list = ns.get('search', []) if dns_addr: for i, val in enumerate(dns_addr): self.pprint(('{title:>'+self.PAD+'} {value}[muted]{mode}[/muted]').format( title='DNS Addresses:' if i == 0 else '', value=val, mode=' ({})'.format(dns_mode) if dns_mode else '', )) if dns_search: for i, val in enumerate(dns_search): self.pprint(('{title:>'+self.PAD+'} {value}').format( title='DNS Search:' if i == 0 else '', value=val, )) self.pprint() def _display_interface_header(self, ifname: str, data): state = data.get('operstate', 'UNKNOWN') + '/' + data.get('adminstate', 'UNKNOWN') scolor = 'unknown' if state == 'UP/UP': state = 'UP' scolor = 'online' elif state == 'DOWN/DOWN': state = 'DOWN' scolor = 'offline' full_type = data.get('type', 'other') ssid = data.get('ssid') tunnel_mode = data.get('tunnel_mode') if full_type == 'wifi' and ssid: full_type += ('/"' + ssid + '"') elif full_type == 'tunnel' and tunnel_mode: full_type += ('/' + tunnel_mode) format = '[{col}]●[/{col}] {idx:>2}: {name} {type} [{col}]{state}[/{col}] ({backend}{netdef})' netdef = ': [highlight]{}[/highlight]'.format(data.get('id')) if data.get('id') else '' extra = '' sign = '' if self.diff: if self._is_interface_missing_in_netplan(ifname): sign = self.PLUS format = '{sign} [{col}]●[/{col}] {idx:>2}: [green][highlight]{name} {type}' format += ' [{col}]{state}[/{col}] ({backend}{netdef})[/highlight][/green]' else: format = ' [muted]● {idx:>2}: {name} {type} {state} ({backend}{netdef})[/muted]' netdef = ': {}'.format(data.get('id')) if data.get('id') else '' if not self.diff_only or self._has_diff(ifname): self.pprint(format.format( sign=sign, col=scolor, idx=data.get('index', '?'), name=ifname, type=full_type, state=state, backend=data.get('backend', 'unmanaged'), netdef=netdef, extra=extra, )) def _display_mac_address(self, ifname: str, data): if macaddress := data.get('macaddress'): hide_macaddress = False missing_system_macaddress = self._get_missing_system_macaddress(ifname) format = '{title:>'+self.PAD+'} {mac}[muted]{vendor}[/muted]' sign = '' if self.diff and not missing_system_macaddress: format = ' [muted]{title:>'+self.PAD+'} {mac}{vendor}[/muted]' if self.diff_only: hide_macaddress = True elif self.diff and missing_system_macaddress: sign = self.PLUS format = '{sign} {title:>'+self.PAD+'} [green][highlight]{mac}{vendor}[/highlight][/green]' if not hide_macaddress: self.pprint((format).format( sign=sign, title='MAC Address:', mac=macaddress, vendor=' ({})'.format(data.get('vendor', '')) if data.get('vendor') else '', )) if self.diff and missing_system_macaddress: sign = self.MINUS format = '{sign} {title:>'+self.PAD+'} [red][highlight]{mac}{vendor}[/highlight][/red]' self.pprint((format).format( sign=sign, title='', mac=missing_system_macaddress, vendor=' ({})'.format(data.get('vendor', '')) if data.get('vendor') else '', )) def _display_ip_addresses(self, ifname: str, data): lst: list = data.get('addresses', []) addresses_displayed = 0 if lst: missing_netplan_addresses = self._get_missing_netplan_addresses(ifname) for obj in lst: sign = '' hide_address = False ip, extra = list(obj.items())[0] # get first (and only) address prefix = extra.get('prefix', '') flags = [] if extra.get('flags'): # flags flags = extra.get('flags', []) highlight_start = '' highlight_end = '' if not flags or 'dhcp' in flags: highlight_start = '[highlight]' highlight_end = '[/highlight]' address = f'{ip}/{prefix}' if self.diff and address not in missing_netplan_addresses: format = ' [muted]{title:>'+self.PAD+'} {start}{ip}/{prefix}{end}{extra}[/muted]' highlight_start = '' highlight_end = '' if self.diff_only: hide_address = True elif self.diff and address in missing_netplan_addresses: sign = self.PLUS format = '{sign} {title:>'+self.PAD+'} [green]{start}{ip}/{prefix}{extra}{end}[/green]' highlight_start = '[highlight]' highlight_end = '[/highlight]' else: format = '{title:>'+self.PAD+'} {start}{ip}/{prefix}{end}[muted]{extra}[/muted]' if not hide_address: self.pprint((format).format( sign=sign, title='Addresses:' if addresses_displayed == 0 else '', ip=ip, prefix=prefix, extra=' ('+', '.join(flags)+')' if flags else '', start=highlight_start, end=highlight_end, )) addresses_displayed += 1 if diff := self._get_interface_diff(ifname): sign = self.MINUS if missing_addresses := diff.get('system_state', {}).get('missing_addresses'): for ip in missing_addresses: self.pprint(('{sign} {title:>'+self.PAD+'} [highlight][red]{ip}[/red][/highlight]').format( sign=sign, title='Addresses:' if addresses_displayed == 0 else '', ip=ip, )) addresses_displayed += 1 if self._is_missing_dhcp4_address(ifname): self.pprint(('{sign} {title:>'+self.PAD+'} [highlight][red]0.0.0.0/0 (dhcp)[/red][/highlight]').format( sign=sign, title='Addresses:' if addresses_displayed == 0 else '', )) addresses_displayed += 1 if self._is_missing_dhcp6_address(ifname): self.pprint(('{sign} {title:>'+self.PAD+'} [highlight][red]::/0 (dhcp)[/red][/highlight]').format( sign=sign, title='Addresses:' if addresses_displayed == 0 else '', )) def _display_dns_addresses(self, ifname: str, data): lst = data.get('dns_addresses', []) nameservers_displayed = 0 if lst: missing_netplan_nameservers = self._get_missing_netplan_nameservers(ifname) for val in lst: sign = '' hide_nameserver = False if self.diff and val not in missing_netplan_nameservers: format = ' [muted]{title:>'+self.PAD+'} {value}[/muted]' highlight_start = '' highlight_end = '' if self.diff_only: hide_nameserver = True elif self.diff and val in missing_netplan_nameservers: sign = self.PLUS format = '{sign} {title:>'+self.PAD+'} [green]{start}{value}{end}[/green]' highlight_start = '[highlight]' highlight_end = '[/highlight]' else: format = '{title:>'+self.PAD+'} {value}' highlight_start = '' highlight_end = '' if not hide_nameserver: self.pprint((format).format( sign=sign, title='DNS Addresses:' if nameservers_displayed == 0 else '', value=val, start=highlight_start, end=highlight_end )) nameservers_displayed += 1 if self._has_diff(ifname): if missing_nameservers_addresses := self._get_missing_system_nameservers(ifname): sign = self.MINUS for ip in missing_nameservers_addresses: self.pprint(('{sign} {title:>'+self.PAD+'} [red][highlight]{ip}[/highlight][/red]').format( sign=sign, title='DNS Addresses:' if nameservers_displayed == 0 else '', ip=ip, )) nameservers_displayed += 1 def _display_dns_search(self, ifname, data): lst = data.get('dns_search', []) searches_displayed = 0 if lst: missing_netplan_search = self._get_missing_netplan_search(ifname) for i, val in enumerate(lst): sign = '' hide_search = False if self.diff and val not in missing_netplan_search: format = ' [muted]{title:>'+self.PAD+'} {value}[/muted]' highlight_start = '' highlight_end = '' if self.diff_only: hide_search = True elif self.diff and val in missing_netplan_search: sign = self.PLUS format = '{sign} {title:>'+self.PAD+'} [green]{start}{value}{end}[/green]' highlight_start = '[highlight]' highlight_end = '[/highlight]' else: format = '{title:>'+self.PAD+'} {value}' highlight_start = '' highlight_end = '' if not hide_search: self.pprint((format).format( sign=sign, title='DNS Search:' if searches_displayed == 0 else '', value=val, start=highlight_start, end=highlight_end )) searches_displayed += 1 if self._has_diff(ifname): if missing_nameservers_search := self._get_missing_system_search(ifname): sign = self.MINUS for domain in missing_nameservers_search: self.pprint(('{sign} {title:>'+self.PAD+'} [red][highlight]{domain}[/highlight][/red]').format( sign=sign, title='DNS Search:' if searches_displayed == 0 else '', domain=domain, )) searches_displayed += 1 def _display_routes(self, ifname, data): lst = data.get('routes', []) missing_netplan_routes = self._get_missing_netplan_routes(ifname) missing_system_routes = self._get_missing_system_routes(ifname) routes_displayed = 0 if lst: if not self.route_lookup_table_names: self.route_lookup_table_names = utils.route_table_lookup() diff_state = NetplanDiffState(None, None) routes = [diff_state._system_route_to_netplan(route) for route in lst] if not self.verbose: # filter out routes that are not in the main route table routes = filter(lambda r: r.table == 254, routes) for route in routes: hide_route = False default_start = '' default_end = '' if route.to == 'default': default_start = '[highlight]' default_end = '[/highlight]' via = '' if route.via: via = ' via ' + route.via src = '' if route.from_addr: src = ' from ' + route.from_addr metric = '' if route.metric < NetplanRoute._METRIC_UNSPEC_: metric = ' metric ' + str(route.metric) table = '' if self.verbose and route.table > 0: table = ' table {}'.format(self.route_lookup_table_names.get(route.table, route.table)) extra = [] if route.protocol and route.protocol != 'kernel': proto = route.protocol extra.append(proto) if route.scope and route.scope != 'global': scope = route.scope extra.append(scope) if route.type and route.type != 'unicast': type = route.type extra.append(type) sign = '' if self.diff and route not in missing_netplan_routes: format = ' [muted]{title:>'+self.PAD+'} {start}{to}{via}{src}{metric}{table}{end}{extra}[/muted]' default_start = '' default_end = '' if self.diff_only: hide_route = True elif self.diff and route in missing_netplan_routes: sign = self.PLUS format = '{sign} {title:>'+self.PAD+'} [green][highlight]{start}{to}{via}{src}{metric}' format += '{table}{end}{extra}[/highlight][/green]' else: format = '{title:>'+self.PAD+'} {start}{to}{via}{src}{metric}{table}{end}[muted]{extra}[/muted]' if not hide_route: self.pprint(format.format( sign=sign, title='Routes:' if routes_displayed == 0 else '', to=route.to, via=via, src=src, metric=metric, table=table, extra=' ('+', '.join(extra)+')' if extra else '', start=default_start, end=default_end)) routes_displayed += 1 if self.diff: for route in missing_system_routes: via = '' if route.via: via = ' via ' + route.via src = '' if route.from_addr: src = ' from ' + route.from_addr metric = '' if route.metric < NetplanRoute._METRIC_UNSPEC_: metric = ' metric ' + str(route.metric) table = '' if self.verbose and route.table > 0: table = ' table {}'.format(self.route_lookup_table_names.get(route.table, route.table)) extra = [] if route.scope and route.scope != 'global': scope = route.scope extra.append(scope) if route.type and route.type != 'unicast': type = route.type extra.append(type) sign = self.MINUS format = '{sign} {title:>'+self.PAD+'} {start}[red]{to}{via}{src}{metric}{table}{extra}[/red]{end}' self.pprint(format.format( sign=sign, title='Routes:' if routes_displayed == 0 else '', to=route.to, via=via, src=src, metric=metric, table=table, extra=' ('+', '.join(extra)+')' if extra else '', start='[highlight]', end='[/highlight]')) routes_displayed += 1 def _display_bridge(self, ifname, data): val = data.get('bridge') if val: missing_netplan_bridge_link = self._get_missing_netplan_bridge_link(ifname) format = '{title:>'+self.PAD+'} {value}' sign = '' hide_bridge = False if self.diff and not missing_netplan_bridge_link: format = ' [muted]{title:>'+self.PAD+'} {value}[/muted]' if self.diff_only: hide_bridge = True elif self.diff and missing_netplan_bridge_link: sign = self.PLUS format = '{sign} {title:>'+self.PAD+'} [highlight][green]{value}[/green][/highlight]' val = missing_netplan_bridge_link if not hide_bridge: self.pprint((format).format( sign=sign, title='Bridge:', value=val, )) if missing_system_bridge_link := self._get_missing_system_bridge_link(ifname): sign = self.MINUS format = '{sign} {title:>'+self.PAD+'} [highlight][red]{value}[/red][/highlight]' self.pprint((format).format( sign=sign, title='Bridge:', value=missing_system_bridge_link, )) def _display_bond(self, ifname, data): val = data.get('bond') if val: missing_netplan_bond_link = self._get_missing_netplan_bond_link(ifname) format = '{title:>'+self.PAD+'} {value}' sign = '' hide_bond = False if self.diff and not missing_netplan_bond_link: format = ' [muted]{title:>'+self.PAD+'} {value}[/muted]' if self.diff_only: hide_bond = True elif self.diff and missing_netplan_bond_link: sign = self.PLUS format = '{sign} {title:>'+self.PAD+'} [highlight][green]{value}[/green][/highlight]' val = missing_netplan_bond_link if not hide_bond: self.pprint((format).format( sign=sign, title='Bond:', value=val, )) if missing_system_bond_link := self._get_missing_system_bond_link(ifname): sign = self.MINUS format = '{sign} {title:>'+self.PAD+'} [highlight][red]{value}[/red][/highlight]' self.pprint((format).format( sign=sign, title='Bond:', value=missing_system_bond_link, )) def _display_vrf(self, ifname, data): val = data.get('vrf') if val: missing_netplan_vrf_link = self._get_missing_netplan_vrf_link(ifname) format = '{title:>'+self.PAD+'} {value}' sign = '' hide_vrf = False if self.diff and not missing_netplan_vrf_link: format = ' [muted]{title:>'+self.PAD+'} {value}[/muted]' if self.diff_only: hide_vrf = True elif self.diff and missing_netplan_vrf_link: sign = self.PLUS format = '{sign} {title:>'+self.PAD+'} [highlight][green]{value}[/green][/highlight]' val = missing_netplan_vrf_link if not hide_vrf: self.pprint((format).format( sign=sign, title='VRF:', value=val, )) if missing_system_vrf_link := self._get_missing_system_vrf_link(ifname): sign = self.MINUS format = '{sign} {title:>'+self.PAD+'} [highlight][red]{value}[/red][/highlight]' self.pprint((format).format( sign=sign, title='VRF:', value=missing_system_vrf_link, )) def _display_members(self, ifname: str, data): lst = data.get('interfaces', []) members_displayed = 0 if lst: missing_netplan_interfaces = self._get_missing_netplan_members(ifname) for val in lst: sign = '' hide_member = False if self.diff and val not in missing_netplan_interfaces: format = ' [muted]{title:>'+self.PAD+'} {value}[/muted]' highlight_start = '' highlight_end = '' if self.diff_only: hide_member = True elif self.diff and val in missing_netplan_interfaces: sign = self.PLUS format = '{sign} {title:>'+self.PAD+'} [green]{start}{value}{end}[/green]' highlight_start = '[highlight]' highlight_end = '[/highlight]' else: format = '{title:>'+self.PAD+'} {value}' highlight_start = '' highlight_end = '' if not hide_member: self.pprint((format).format( sign=sign, title='Interfaces:' if members_displayed == 0 else '', value=val, start=highlight_start, end=highlight_end, )) members_displayed += 1 if self._has_diff(ifname): if missing_members := self._get_missing_system_members(ifname): sign = self.MINUS for member in missing_members: self.pprint(('{sign} {title:>'+self.PAD+'} [red][highlight]{member}[/highlight][/red]').format( sign=sign, title='Interfaces:' if members_displayed == 0 else '', member=member, )) members_displayed += 1 def _display_activation_mode(self, data): val = data.get('activation_mode') if val: self.pprint(('{title:>'+self.PAD+'} {value}').format( title='Activation Mode:', value=val, )) def _display_missing_interfaces(self): missing_interfaces = self._get_missing_system_interfaces() sign = self.MINUS for index, (interface, properties) in enumerate(missing_interfaces.items(), 1): # If we called netplan status for a single interface, ignore the rest if self.ifname and self.ifname != interface: continue self.pprint('{sign} [{col}]● {idx:>2} {name} {type}[/{col}]'.format( sign=sign, col='red', idx='', name=interface, type=properties.get('type'), )) if index != len(missing_interfaces) and not self.ifname: # linebreak only if it's not the last interfaces or the only one self.pprint() def plain_print(self, *args, **kwargs): if len(args): lst = list(args) for tag in MATCH_TAGS.findall(lst[0]): # remove matching opening and closing tag lst[0] = lst[0].replace('[{}]'.format(tag), '')\ .replace('[/{}]'.format(tag), '') return print(*lst, **kwargs) return print(*args, **kwargs) def pretty_print(self, data: JSON, total: int, _console_width=None) -> None: self.pprint = self._create_pretty_print(_console_width) self.PLUS = '[green]+[/green]' self.MINUS = '[red]-[/red]' self.PAD = '18' if self.diff: # In diff mode we shift the text 2 columns to the right so we can display # + and - and maintain the alignment consistency self.PAD = '20' # Global state if not self.diff: self._display_global_state(data) # Per interface interfaces = [(key, data[key]) for key in data if key != 'netplan-global-state'] if self.diff_only: # in diff-only mode we filter out interfaces that don't have any diff interfaces = list(filter(lambda i: self._has_diff(i[0]), interfaces)) missing_interfaces = self._get_missing_system_interfaces() for index, (ifname, ifconfig) in enumerate(interfaces, 1): # If we called netplan status for a single interface, ignore the rest if self.ifname and self.ifname != ifname: continue self._display_interface_header(ifname, ifconfig) self._display_mac_address(ifname, ifconfig) self._display_ip_addresses(ifname, ifconfig) self._display_dns_addresses(ifname, ifconfig) self._display_dns_search(ifname, ifconfig) self._display_routes(ifname, ifconfig) self._display_bridge(ifname, ifconfig) self._display_bond(ifname, ifconfig) self._display_vrf(ifname, ifconfig) self._display_members(ifname, ifconfig) self._display_activation_mode(ifconfig) if not self.diff_only or self._has_diff(ifname): # we only break to a new line if we still have data to display if (index != len(interfaces) or len(missing_interfaces) > 0) and not self.ifname: self.pprint() if self.diff: self._display_missing_interfaces() hidden = total - len(interfaces) if hidden > 0 and not self.diff: self.pprint('\n{} inactive interfaces hidden. Use "--all" to show all.'.format(hidden)) if self.diff and not self.diff_only: self.pprint( '\nUse [yellow]"--diff-only"[/yellow] to omit the information that is consistent between the system and Netplan.' ) def command(self): # --diff-only implies --diff if self.diff_only: self.diff = True # --diff needs data from all interfaces to work if self.diff: self.all = True system_state = SystemConfigState(self.ifname, self.all) output_format = self.format.lower() if self.diff: netplan_state = NetplanConfigState(rootdir=self.root_dir) diff_state = NetplanDiffState(system_state, netplan_state) self.state_diff = diff_state.get_diff(self.ifname) if output_format == 'json': print(json.dumps(self.state_diff, cls=DiffJSONEncoder)) return elif output_format == 'yaml': serialized = json.dumps(self.state_diff, cls=DiffJSONEncoder) print(yaml.dump(json.loads(serialized))) return if output_format == 'json': # structural JSON output print(json.dumps(system_state.get_data())) elif output_format == 'yaml': # stuctural YAML output print(yaml.dump(system_state.get_data())) else: # pretty print, human readable output self.pretty_print(system_state.get_data(), system_state.number_of_interfaces) netplan-1.0/netplan_cli/cli/commands/try_command.py000066400000000000000000000175171457004145200225560ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan try command line''' import logging import netplan import os import time import shutil import signal import sys import tempfile from ...configmanager import ConfigManager from .. import utils from .apply import NetplanApply from ... import terminal # Keep a timeout long enough to allow the network to converge, 60 seconds may # be slightly short given some complex configs, i.e. if STP must reconverge. DEFAULT_INPUT_TIMEOUT = 120 class NetplanTry(utils.NetplanCommand): def __init__(self): super().__init__(command_id='try', description='Try to apply a new netplan config to running ' 'system, with automatic rollback', leaf=True) self.configuration_changed = False self.new_interfaces = None self.config_file = None self._config_manager = None self.t_settings = None self.t = None self._rootdir = os.environ.get('DBUS_TEST_NETPLAN_ROOT', '/') self._netplan_try_stamp = os.path.join(self._rootdir, 'run', 'netplan', 'netplan-try.ready') @property def config_manager(self): # pragma: nocover (called by later commands) if not self._config_manager: self._config_manager = ConfigManager(prefix=self._rootdir) return self._config_manager def clear_ready_stamp(self): if os.path.isfile(self._netplan_try_stamp): os.remove(self._netplan_try_stamp) return True return False def touch_ready_stamp(self): os.makedirs(self._rootdir + '/run/netplan', mode=0o700, exist_ok=True) open(self._netplan_try_stamp, 'w').close() def run(self): # pragma: nocover (requires user input) self.parser.add_argument('--config-file', help='Apply the config file in argument in addition to current configuration.') self.parser.add_argument('--timeout', type=int, default=DEFAULT_INPUT_TIMEOUT, help="Maximum number of seconds to wait for the user's confirmation") self.parser.add_argument('--state', help='Directory containing previous YAML configuration') self.func = self.command_try self.parse_args() self.run_command() def command_try(self): # pragma: nocover (requires user input) if not self.is_revertable(): sys.exit(os.EX_CONFIG) try: fd = sys.stdin.fileno() self.t = terminal.Terminal(fd) self.t.save(self.t_settings) # we really don't want to be interrupted while doing backup/revert operations signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGUSR1, self._signal_handler) self.backup() self.setup() NetplanApply().command_apply(run_generate=True, sync=True, exit_on_error=False, state_dir=self.state) # Touch stamp file, it is the signal (for netplan-dbus) that we're # ready to accept any Accept/Reject input (like SIGUSR1 or SIGTERM) self.touch_ready_stamp() self.t.get_confirmation_input(timeout=self.timeout) except terminal.InputRejected: print("\nReverting.") self.revert() except terminal.InputAccepted: print("\nConfiguration accepted.") except Exception as e: print("\nAn error occurred: %s" % e) print("\nReverting.") self.revert() finally: if self.t: self.t.reset(self.t_settings) self.cleanup() self.clear_ready_stamp() def backup(self): # pragma: nocover (requires user input) backup_config_dir = False if self.config_file: backup_config_dir = True self.config_manager.backup(backup_config_dir=backup_config_dir) def setup(self): # pragma: nocover (requires user input) if self.config_file: dest_dir = os.path.join("/", "etc", "netplan") dest_name = os.path.basename(self.config_file).rstrip('.yaml') dest_suffix = time.time() dest_path = os.path.join(dest_dir, "{}.{}.yaml".format(dest_name, dest_suffix)) self.config_manager.add({self.config_file: dest_path}) self.configuration_changed = True def revert(self): # pragma: nocover (requires user input) # backup the state we just tried to apply tempdir = tempfile.mkdtemp() confdir = os.path.join(tempdir, 'etc', 'netplan') os.makedirs(confdir) shutil.copytree('/etc/netplan', confdir, dirs_exist_ok=True) # restore previous state self.config_manager.revert() NetplanApply().command_apply(run_generate=False, sync=True, exit_on_error=False, state_dir=tempdir) # clear the backup shutil.rmtree(tempdir) def cleanup(self): # pragma: nocover (requires user input) self.config_manager.cleanup() def is_revertable(self): ''' Check if the configuration is revertable, if it doesn't contain bits that we know are likely to render the system unstable if we apply it, or if we revert. Returns True if the parsed config is "revertable", meaning that we can actually rely on backends to re-apply /all/ of the relevant configuration to interfaces when their config changes. Returns False if the parsed config contains options that are known to not cleanly revert via the backend. ''' extra_config = [] if self.config_file: extra_config.append(self.config_file) np_state = None try: np_state = self.config_manager.parse(extra_config=extra_config) except utils.config_errors as e: logging.error(e) sys.exit(os.EX_CONFIG) revert_unsupported = [] # Bridges and bonds are special. They typically include (or could include) # more than one device in them, and they can be set with special parameters # to tweak their behavior, which are really hard to "revert", especially # as systemd-networkd doesn't necessarily touch them when config changes. multi_iface: dict[str, netplan.NetDefinition] = {} multi_iface.update(np_state.bridges) multi_iface.update(np_state.bonds) for itf in multi_iface.values(): if not itf._is_trivial_compound_itf: reason = "reverting custom parameters for bridges and bonds is not supported" revert_unsupported.append((itf.id, reason)) if revert_unsupported: for ifname, reason in revert_unsupported: print("{}: {}".format(ifname, reason)) print("\nPlease carefully review the configuration and use 'netplan apply' directly.") return False return True def _signal_handler(self, sig, frame): # pragma: nocover (requires user input) if sig == signal.SIGUSR1: raise terminal.InputAccepted() else: if self.configuration_changed: raise terminal.InputRejected() netplan-1.0/netplan_cli/cli/core.py000066400000000000000000000041351457004145200173610ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Martin Pitt # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan command line''' import logging import os from . import utils from netplan import NetplanException, NetplanValidationException, NetplanParserException FALLBACK_PATH = '/usr/bin:/snap/bin' class Netplan(utils.NetplanCommand): def __init__(self): super().__init__(command_id='', description='Network configuration in YAML', leaf=False) os.environ.update({ 'LC_ALL': 'C', 'PATH': os.getenv('PATH', FALLBACK_PATH)}) def parse_args(self): from . import commands as cli_commands self._import_subcommands(cli_commands) super().parse_args() def main(self): self.parse_args() if self.debug: logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') os.environ['G_MESSAGES_DEBUG'] = 'all' else: logging.basicConfig(level=logging.INFO, format='%(message)s') try: self.run_command() except NetplanParserException as e: message = f'{e.filename}:{e.line}:{e.column}: {e}' logging.warning(f'Command failed: {message}') except NetplanValidationException as e: logging.warning(f'Command failed: {e.filename}: {e}') except NetplanException as e: logging.warning(f'Command failed: {e}') netplan-1.0/netplan_cli/cli/ovs.py000066400000000000000000000206771457004145200172510ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2020 Canonical, Ltd. # Author: Lukas 'slyon' Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import logging import os import subprocess import re from .utils import systemctl_is_active, systemctl_is_installed OPENVSWITCH_OVS_VSCTL = '/usr/bin/ovs-vsctl' OPENVSWITCH_OVSDB_SERVER_UNIT = 'ovsdb-server.service' # Defaults for non-optional settings, as defined here: # http://www.openvswitch.org/ovs-vswitchd.conf.db.5.pdf DEFAULTS = { # Mandatory columns: 'mcast_snooping_enable': 'false', 'rstp_enable': 'false', } GLOBALS = { # Global commands: 'set-ssl': ('del-ssl', 'get-ssl'), 'set-fail-mode': ('del-fail-mode', 'get-fail-mode'), 'set-controller': ('del-controller', 'get-controller'), } class OvsDbServerNotRunning(Exception): pass class OvsDbServerNotInstalled(Exception): pass def _del_col(type, iface, column, value): """Cleanup values from a column (i.e. "column=value")""" default = DEFAULTS.get(column) if default is None: # removes the exact value only if it was set by netplan subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, value]) elif default and default != value: # reset to default, if its not the default already subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'set', type, iface, '%s=%s' % (column, default)]) def _del_dict(type, iface, column, key, value): """Cleanup values from a dictionary (i.e. "column:key=value")""" # removes the exact value only if it was set by netplan subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, key, _escape_colon(value)]) # for ovsdb remove: column key's value can not contain bare ':', need to escape with '\' def _escape_colon(literal): return re.sub(r'([^\\]):', r'\g<1>\:', literal) def _del_global(type, iface, key, value): """Cleanup commands from the global namespace""" del_cmd, get_cmd = GLOBALS.get(key, (None, None)) if del_cmd == 'del-ssl': iface = None if del_cmd: args_get = [OPENVSWITCH_OVS_VSCTL, get_cmd] args_del = [OPENVSWITCH_OVS_VSCTL, del_cmd] if iface: args_get.append(iface) args_del.append(iface) # Check the current value of a global command and compare it to the tag-value, e.g.: # * get-ssl: netplan/global/set-ssl=/private/key.pem,/another/cert.pem,/some/ca-cert.pem # Private key: /private/key.pem # Certificate: /another/cert.pem # CA Certificate: /some/ca-cert.pem # Bootstrap: false # * get-fail-mode: netplan/global/set-fail-mode=secure # secure # * get-controller: netplan/global/set-controller=tcp:127.0.0.1:1337,unix:/some/socket # tcp:127.0.0.1:1337 # unix:/some/socket out = subprocess.check_output(args_get, text=True) # Clean it only if the exact same value(s) were set by netplan. # Don't touch it if other values were set by another integration. if all(item in out for item in value.split(',')): subprocess.check_call(args_del) else: raise Exception('Reset command unknown for:', key) def clear_setting(type, iface, setting, value): """Check if this setting is in a dict or a colum and delete accordingly""" split = setting.split('/', 2) col = split[1] if col == 'global' and len(split) > 2: _del_global(type, iface, split[2], value) elif len(split) > 2: _del_dict(type, iface, split[1], split[2], value) else: _del_col(type, iface, split[1], value) # Cleanup the tag itself (i.e. "netplan/column[/key]") subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, 'external-ids', setting]) def is_ovs_interface(iface, np_interface_dict): assert isinstance(np_interface_dict, dict) np_def = np_interface_dict.get(iface, None) return np_def and np_def.backend == 'OpenVSwitch' def apply_ovs_cleanup(config_manager, ovs_old, ovs_current): # pragma: nocover (covered in autopkgtest) """ Query OpenVSwitch state through 'ovs-vsctl' and filter for netplan=true tagged ports/bonds and bridges. Delete interfaces which are not defined in the current configuration. Also filter for individual settings tagged netplan/[/ 0: subprocess.check_call([OPENVSWITCH_OVS_VSCTL, '--if-exists', 'del-bond-iface', iface]) else: subprocess.check_call([OPENVSWITCH_OVS_VSCTL, '--if-exists', t[1], iface]) # Step 2: Clean up the settings of the remaining interfaces for t in ('Port', 'Bridge', 'Interface', 'Open_vSwitch', 'Controller'): cols = 'name,external-ids' if t == 'Open_vSwitch': cols = 'external-ids' elif t == 'Controller': cols = '_uuid,external-ids' # handle _uuid as if it would be the iface 'name' out = subprocess.check_output([OPENVSWITCH_OVS_VSCTL, '--columns=%s' % cols, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', t], text=True) for line in out.splitlines(): if 'netplan/' in line: iface = '.' extids = line if t != 'Open_vSwitch': iface, extids = line.split(',', 1) # Check each line (interface) if it contains any netplan tagged settings, e.g.: # ovs0,"iface-id=myhostname netplan=true netplan/external-ids/iface-id=myhostname" # ovs1,"netplan=true netplan/global/set-fail-mode=standalone netplan/mcast_snooping_enable=false" for entry in extids.strip('"').split(' '): if entry.startswith('netplan/') and '=' in entry: setting, val = entry.split('=', 1) clear_setting(t, iface, setting, val) # Show the warning only if we are or have been working with OVS definitions elif ovs_old or ovs_current: logging.warning('ovs-vsctl is missing, cannot tear down old OpenVSwitch interfaces') netplan-1.0/netplan_cli/cli/sriov.py000066400000000000000000000453231457004145200175770ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2020-2022 Canonical, Ltd. # Author: Łukasz 'sil2100' Zemczak # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import json import logging import os import subprocess import typing from collections import defaultdict from . import utils from ..configmanager import ConfigurationError import netplan import netifaces # PCIDevice class originates from mlnx_switchdev_mode/sriovify.py # Copyright 2019 Canonical Ltd, Apache License, Version 2.0 # https://github.com/openstack-charmers/mlnx-switchdev-mode class PCIDevice(object): """Helper class for interaction with a PCI device""" def __init__(self, pci_addr: str): """Initialise a new PCI device handler :param pci_addr: PCI address of device :type: str """ self.pci_addr = pci_addr @property def sys(self) -> str: """sysfs path (can be overridden for testing) :return: full path to /sys filesystem :rtype: str """ return "/sys" @property def path(self) -> str: """/sys path for PCI device :return: full path to PCI device in /sys filesystem :rtype: str """ return os.path.join(self.sys, "bus/pci/devices", self.pci_addr) def subpath(self, subpath: str) -> str: """/sys subpath helper for PCI device :param subpath: subpath to construct path for :type: str :return: self.path + subpath :rtype: str """ return os.path.join(self.path, subpath) @property def driver(self) -> str: """Kernel driver for PCI device :return: kernel driver in use for device :rtype: str """ driver = '' if os.path.exists(self.subpath("driver")): driver = os.path.basename(os.readlink(self.subpath("driver"))) return driver @property def bound(self) -> bool: """Determine if device is bound to a kernel driver :return: whether device is bound to a kernel driver :rtype: bool """ return os.path.exists(self.subpath("driver")) @property def is_pf(self) -> bool: """Determine if device is a SR-IOV Physical Function :return: whether device is a PF :rtype: bool """ return os.path.exists(self.subpath("sriov_numvfs")) @property def is_vf(self) -> bool: """Determine if device is a SR-IOV Virtual Function :return: whether device is a VF :rtype: bool """ return os.path.exists(self.subpath("physfn")) @property def vf_addrs(self) -> list: """List Virtual Function addresses associated with a Physical Function :return: List of PCI addresses of Virtual Functions :rtype: list[str] """ vf_addrs = [] i = 0 while True: try: vf_addrs.append( os.path.basename( os.readlink(self.subpath("virtfn{}".format(i))) ) ) except FileNotFoundError: break i += 1 return vf_addrs @property def vfs(self) -> list: """List Virtual Function associated with a Physical Function :return: List of PCI devices of Virtual Functions :rtype: list[PCIDevice] """ return [PCIDevice(addr) for addr in self.vf_addrs] def devlink_set(self, obj_name: str, prop: str, value: str): """Set devlink options for the PCI device :param obj_name: devlink object to set options on :type: str :param prop: property to set :type: str :param value: value to set for property :type: str """ subprocess.check_call( [ "/sbin/devlink", "dev", obj_name, "set", "pci/{}".format(self.pci_addr), prop, value, ] ) def devlink_eswitch_mode(self) -> str: """Query eswitch mode via devlink for the PCI device :return: the eswitch mode or '__undetermined' if it can't be retrieved :rtype: str """ pci = f"pci/{self.pci_addr}" try: output = subprocess.check_output( [ "/sbin/devlink", "-j", "dev", "eswitch", "show", pci, ], stderr=subprocess.DEVNULL, ) except subprocess.CalledProcessError: return '__undetermined' json_output = json.loads(output) # The JSON document looks like this when the 'mode' is available: # {"dev":{"pci/0000:03:00.0":{"mode":"switchdev"}}} # and like this when it's not available # {"dev":{}} return json_output.get("dev", {}).get(pci, {}).get('mode', '__undetermined') def __str__(self) -> str: """String represenation of object :return: PCI address of string :rtype: str """ return self.pci_addr def bind_vfs(vfs: typing.Iterable[PCIDevice], driver): """Bind unbound VFs to driver.""" bound_vfs = [] for vf in vfs: if not vf.bound: with open("/sys/bus/pci/drivers/{}/bind".format(driver), "wt") as f: f.write(vf.pci_addr) bound_vfs.append(vf) return bound_vfs def unbind_vfs(vfs: typing.Iterable[PCIDevice], driver) -> typing.Iterable[PCIDevice]: """Unbind bound VFs from driver.""" unbound_vfs = [] for vf in vfs: if vf.bound: with open("/sys/bus/pci/drivers/{}/unbind".format(driver), "wt") as f: f.write(vf.pci_addr) unbound_vfs.append(vf) return unbound_vfs def _get_target_interface(interfaces, np_state, pf_link, pfs): if pf_link not in pfs: # handle the match: syntax, get the actual device name pf_dev = np_state[pf_link] if pf_dev._has_match: # now here it's a bit tricky set_name = pf_dev.set_name if set_name and set_name in interfaces: # if we had a match: stanza and set-name: this means we should # assume that, if found, the interface has already been # renamed - use the new name pfs[pf_link] = set_name else: for interface in interfaces: if not pf_dev._match_interface( iface_name=interface, iface_driver=utils.get_interface_driver_name(interface), iface_mac=utils.get_interface_macaddress(interface)): continue # we have a matching PF # store the matching interface in the dictionary of # active PFs, but error out if we matched more than one if pf_link in pfs: raise ConfigurationError('matched more than one interface for a PF device: %s' % pf_link) pfs[pf_link] = interface else: # no match field, assume entry name is the interface name if pf_link in interfaces: pfs[pf_link] = pf_link return pfs.get(pf_link, None) def _get_pci_slot_name(netdev): """ Read PCI slot name for given interface name """ uevent_path = os.path.join('/sys/class/net', netdev, 'device/uevent') try: with open(uevent_path) as f: pci_slot_name = None for line in f.readlines(): line = line.strip() if line.startswith('PCI_SLOT_NAME='): pci_slot_name = line.split('=', 2)[1] return pci_slot_name except IOError as e: raise RuntimeError('failed parsing PCI slot name for %s: %s' % (netdev, str(e))) def get_vf_count_and_functions(interfaces, np_state, vf_counts, vfs, pfs): """ Go through the list of netplan ethernet devices and identify which are PFs and VFs, matching the former with actual networking interfaces. Count how many VFs each PF will need. """ for nid, netdef in np_state.ethernets.items(): if netdef.links.get('sriov') and _get_target_interface(interfaces, np_state, netdef.links.get('sriov').id, pfs): vfs[nid] = None try: count = netdef._vf_count except netplan.NetplanException as e: raise ConfigurationError(str(e)) if count == 0: continue pf = _get_target_interface(interfaces, np_state, nid, pfs) if pf: vf_counts[pf] = count def set_numvfs_for_pf(pf, vf_count): """ Allocate the required number of VFs for the selected PF. """ if vf_count > 256: raise ConfigurationError( 'cannot allocate more VFs for PF %s than the SR-IOV maximum: %s > 256' % (pf, vf_count)) devdir = os.path.join('/sys/class/net', pf, 'device') numvfs_path = os.path.join(devdir, 'sriov_numvfs') totalvfs_path = os.path.join(devdir, 'sriov_totalvfs') try: with open(totalvfs_path) as f: vf_max = int(f.read().strip()) except IOError as e: raise RuntimeError('failed parsing sriov_totalvfs for %s: %s' % (pf, str(e))) except ValueError: raise RuntimeError('invalid sriov_totalvfs value for %s' % pf) if vf_count > vf_max: raise ConfigurationError( 'cannot allocate more VFs for PF %s than supported: %s > %s (sriov_totalvfs)' % (pf, vf_count, vf_max)) try: with open(numvfs_path, 'w') as f: f.write(str(vf_count)) except IOError as e: bail = True if e.errno == 16: # device or resource busy logging.warning('device or resource busy while setting sriov_numvfs for %s, trying workaround' % pf) try: # doing this in two open/close sequences so that # it's as close to writing via shell as possible with open(numvfs_path, 'w') as f: f.write('0') with open(numvfs_path, 'w') as f: f.write(str(vf_count)) except IOError as e_inner: e = e_inner else: bail = False if bail: raise RuntimeError('failed setting sriov_numvfs to %s for %s: %s' % (vf_count, pf, str(e))) return True def perform_hardware_specific_quirks(pf): """ Perform any hardware-specific quirks for the given SR-IOV device to make sure all the VF-count changes are applied. """ devdir = os.path.join('/sys/class/net', pf, 'device') try: with open(os.path.join(devdir, 'vendor')) as f: device_id = f.read().strip()[2:] with open(os.path.join(devdir, 'device')) as f: vendor_id = f.read().strip()[2:] except IOError as e: raise RuntimeError('could not determine vendor and device ID of %s: %s' % (pf, str(e))) combined_id = ':'.join([vendor_id, device_id]) quirk_devices = () # TODO: add entries to the list if combined_id in quirk_devices: # pragma: nocover (empty quirk_devices) # some devices need special handling, so this is the place # Currently this part is empty, but has been added as a preemptive # measure, as apparently a lot of SR-IOV cards have issues with # dynamically allocating VFs. Some cards seem to require a full # kernel module reload cycle after changing the sriov_numvfs value # for the changes to come into effect. # Any identified card/vendor can then be special-cased here, if # needed. pass def apply_vlan_filter_for_vf(pf, vf, vlan_name, vlan_id, prefix='/'): """ Apply the hardware VLAN filtering for the selected VF. """ # this is more complicated, because to do this, we actually need to have # the vf index - just knowing the vf interface name is not enough vf_index = None # the prefix argument is here only for unit testing purposes vf_devdir = os.path.join(prefix, 'sys/class/net', vf, 'device') vf_dev_id = os.path.basename(os.readlink(vf_devdir)) pf_devdir = os.path.join(prefix, 'sys/class/net', pf, 'device') for f in os.listdir(pf_devdir): if 'virtfn' in f: dev_path = os.path.join(pf_devdir, f) dev_id = os.path.basename(os.readlink(dev_path)) if dev_id == vf_dev_id: vf_index = f[6:] break if not vf_index: raise RuntimeError( 'could not determine the VF index for %s while configuring vlan %s' % (vf, vlan_name)) # now, create the VLAN filter # TODO: would be best if we did this directl via python, without calling # the iproute tooling try: subprocess.check_call(['ip', 'link', 'set', 'dev', pf, 'vf', vf_index, 'vlan', str(vlan_id)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError: raise RuntimeError( 'failed setting SR-IOV VLAN filter for vlan %s (ip link set command failed)' % vlan_name) def apply_sriov_config(config_manager, rootdir='/'): """ Go through all interfaces, identify which ones are SR-IOV VFs, create them and perform all other necessary setup. """ config_manager.parse() interfaces = netifaces.interfaces() np_state = config_manager.np_state # for sr-iov devices, we identify VFs by them having a link: field # pointing to an PF. So let's browse through all ethernet devices, # find all that are VFs and count how many of those are linked to # particular PFs, as we need to then set the numvfs for each. vf_counts = defaultdict(int) # we also store all matches between VF/PF netplan entry names and # interface that they're currently matching to vfs = {} pfs = {} get_vf_count_and_functions( interfaces, np_state, vf_counts, vfs, pfs) # setup the required number of VFs per PF # at the same time store which PFs got changed in case the NICs # require some special quirks for the VF number to change vf_count_changed = [] if vf_counts: for pf, vf_count in vf_counts.items(): if not set_numvfs_for_pf(pf, vf_count): continue vf_count_changed.append(pf) if vf_count_changed: # some cards need special treatment when we want to change the # number of enabled VFs for pf in vf_count_changed: perform_hardware_specific_quirks(pf) # also, since the VF number changed, the interfaces list also # changed, so we need to refresh it interfaces = netifaces.interfaces() # now in theory we should have all the new VFs set up and existing; # this is needed because we will have to now match the defined VF # entries to existing interfaces, otherwise we won't be able to set # filtered VLANs for those. # XXX: does matching those even make sense? for vf in vfs: netdef = np_state[vf] if netdef._has_match: # right now we only match by name, as I don't think matching per # driver and/or macaddress makes sense # TODO: print warning if other matches are provided for interface in interfaces: if netdef._match_interface(iface_name=interface): if vf in vfs and vfs[vf]: raise ConfigurationError('matched more than one interface for a VF device: %s' % vf) vfs[vf] = interface else: if vf in interfaces: vfs[vf] = vf # Walk the SR-IOV PFs and check if we need to change the eswitch mode for netdef_id, iface in pfs.items(): netdef = np_state[netdef_id] eswitch_mode = netdef._embedded_switch_mode if eswitch_mode in ['switchdev', 'legacy']: pci_addr = _get_pci_slot_name(iface) pcidev = PCIDevice(pci_addr) current_eswitch_mode_system = pcidev.devlink_eswitch_mode() if eswitch_mode != current_eswitch_mode_system: if pcidev.is_pf: logging.debug("Found VFs of {}: {}".format(pcidev, pcidev.vf_addrs)) if pcidev.vfs: rebind_delayed = netdef._delay_virtual_functions_rebind try: unbind_vfs(pcidev.vfs, pcidev.driver) pcidev.devlink_set('eswitch', 'mode', eswitch_mode) finally: if not rebind_delayed: bind_vfs(pcidev.vfs, pcidev.driver) filtered_vlans_set = set() for vlan, netdef in np_state.vlans.items(): # there is a special sriov vlan renderer that one can use to mark # a selected vlan to be done in hardware (VLAN filtering) if netdef._has_sriov_vlan_filter: # this only works for SR-IOV VF interfaces link = netdef.links.get('vlan') vlan_id = netdef._vlan_id vf = vfs.get(link.id) if not vf: # it is possible this is not an error, for instance when # the configuration has been defined 'for the future' # XXX: but maybe we should error out here as well? logging.warning( 'SR-IOV vlan defined for %s but link %s is either not a VF or has no matches' % (vlan, link.id)) continue # get the parent pf interface # first we fetch the related vf netplan entry # and finally, get the matched pf interface pf = pfs.get(link.links.get('sriov').id) if vf in filtered_vlans_set: raise ConfigurationError( 'interface %s for netplan device %s (%s) already has an SR-IOV vlan defined' % (vf, link.id, vlan)) # TODO: make sure that we don't apply the filter twice apply_vlan_filter_for_vf(pf, vf, vlan, vlan_id) filtered_vlans_set.add(vf) netplan-1.0/netplan_cli/cli/state.py000066400000000000000000000556121457004145200175570ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2023 Canonical, Ltd. # Authors: Lukas Märdian # Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from collections import defaultdict, namedtuple import ipaddress import json import logging import re import socket import subprocess import sys from io import StringIO from typing import Dict, List, Type, Union import yaml import dbus import netplan from . import utils JSON = Union[Dict[str, 'JSON'], List['JSON'], int, str, float, bool, Type[None]] DEVICE_TYPES = { 'bond': 'bond', 'bridge': 'bridge', 'dummy': 'dummy-device', 'erspan': 'tunnel', 'ether': 'ethernet', 'gretap': 'tunnel', 'ipgre': 'tunnel', 'ip6gre': 'tunnel', 'loopback': 'ethernet', 'sit': 'tunnel', 'tunnel': 'tunnel', 'tun': 'tunnel', 'tunnel6': 'tunnel', 'wireguard': 'tunnel', 'wlan': 'wifi', 'wwan': 'modem', 'veth': 'virtual-ethernet', 'vlan': 'vlan', 'vrf': 'vrf', 'vxlan': 'tunnel', # Netplan netdef types 'wifis': 'wifi', 'ethernets': 'ethernet', 'bridges': 'bridge', 'bonds': 'bond', 'nm-devices': 'nm-device', 'dummy-devices': 'dummy-device', 'modems': 'modem', 'vlans': 'vlan', 'vrfs': 'vrf', } class Interface(): def __extract_mac(self, ip: dict) -> str: ''' Extract the MAC address if it's set inside the JSON data and seems to have the correct format. Return 'None' otherwise. ''' if len(address := ip.get('address', '')) == 17: # 6 byte MAC (+5 colons) return address.lower() return None def __init__(self, ip: dict, nd_data: JSON = [], nm_data: JSON = [], resolved_data: tuple = (None, None), route_data: tuple = (None, None)): self.idx: int = ip.get('ifindex', -1) self.name: str = ip.get('ifname', 'unknown') self.adminstate: str = 'UP' if 'UP' in ip.get('flags', []) else 'DOWN' self.operstate: str = ip.get('operstate', 'unknown').upper() self.macaddress: str = self.__extract_mac(ip) self.bridge: str = None self.bond: str = None self.vrf: str = None self.members: List[str] = [] # Filter networkd/NetworkManager data nm_data = nm_data or [] # avoid 'None' value on systems without NM self.nd: JSON = next((x for x in nd_data if x['Index'] == self.idx), None) self.nm: JSON = next((x for x in nm_data if x['device'] == self.name), None) # Filter resolved's DNS data self.dns_addresses: list = None if resolved_data[0]: self.dns_addresses = [] for itr in resolved_data[0]: if int(itr[0]) == int(self.idx): ipfamily = itr[1] dns = itr[2] self.dns_addresses.append(socket.inet_ntop(ipfamily, b''.join([v.to_bytes(1, 'big') for v in dns]))) self.dns_search: list = None if resolved_data[1]: self.dns_search = [] for v in resolved_data[1]: if int(v[0]) == int(self.idx): self.dns_search.append(str(v[1])) # Filter route data _routes: list = [] self.routes: list = None if route_data[0]: _routes += route_data[0] if route_data[1]: _routes += route_data[1] if _routes: self.routes = [] for obj in _routes: if obj.get('dev') == self.name: elem = {'to': obj.get('dst')} if val := obj.get('family'): elem['family'] = val if val := obj.get('gateway'): elem['via'] = val if val := obj.get('prefsrc'): elem['from'] = val if val := obj.get('metric'): elem['metric'] = val if val := obj.get('type'): elem['type'] = val if val := obj.get('scope'): elem['scope'] = val if val := obj.get('protocol'): elem['protocol'] = val if val := obj.get('table'): elem['table'] = val self.routes.append(elem) self.addresses: list = None if addr_info := ip.get('addr_info'): self.addresses = [] for addr in addr_info: flags: list = [] if ipaddress.ip_address(addr['local']).is_link_local: flags.append('link') if self.routes: for route in self.routes: if ('from' in route and ipaddress.ip_address(route['from']) == ipaddress.ip_address(addr['local'])): if route['protocol'] == 'dhcp': flags.append('dhcp') break ip_addr = addr['local'].lower() elem = {ip_addr: {'prefix': addr['prefixlen']}} if flags: elem[ip_addr]['flags'] = flags self.addresses.append(elem) self.iproute_type: str = None if info_kind := ip.get('linkinfo', {}).get('info_kind'): self.iproute_type = info_kind.strip() # workaround: query some data which is not available via networkctl's JSON output self._networkctl: str = self.query_networkctl(self.name) or '' def query_nm_ssid(self, con_name: str) -> str: ssid: str = None try: ssid = utils.nmcli_out(['--get-values', '802-11-wireless.ssid', 'con', 'show', 'id', con_name]) return ssid.strip() except Exception as e: logging.warning('Cannot query NetworkManager SSID for {}: {}'.format( con_name, str(e))) return ssid def query_networkctl(self, ifname: str) -> str: output: str = None try: output = subprocess.check_output(['networkctl', 'status', '--', ifname], text=True) except Exception as e: logging.warning('Cannot query networkctl for {}: {}'.format( ifname, str(e))) return output def json(self) -> JSON: json = { 'index': self.idx, 'adminstate': self.adminstate, 'operstate': self.operstate, } if self.type: json['type'] = self.type if self.ssid: json['ssid'] = self.ssid if self.tunnel_mode: json['tunnel_mode'] = self.tunnel_mode if self.backend: json['backend'] = self.backend if self.netdef_id: json['id'] = self.netdef_id if self.macaddress: json['macaddress'] = self.macaddress if self.vendor: json['vendor'] = self.vendor if self.addresses: json['addresses'] = self.addresses if self.dns_addresses: json['dns_addresses'] = self.dns_addresses if self.dns_search: json['dns_search'] = self.dns_search if self.routes: json['routes'] = self.routes if self.activation_mode: json['activation_mode'] = self.activation_mode if self.bridge: json['bridge'] = self.bridge if self.bond: json['bond'] = self.bond if self.vrf: json['vrf'] = self.vrf if self.members: json['interfaces'] = self.members return (self.name, json) @property def up(self) -> bool: return self.adminstate == 'UP' and self.operstate == 'UP' @property def down(self) -> bool: return self.adminstate == 'DOWN' and self.operstate == 'DOWN' @property def type(self) -> str: nd_type = self.nd.get('Type') if self.nd else None if nd_type == 'none': # If the Type is reported as 'none' by networkd, the interface still might have a Kind. nd_type = self.nd.get('Kind') if nd_type == 'ether': # There are different kinds of 'ether' devices, such as VRFs, veth and dummies if kind := self.nd.get('Kind'): nd_type = kind if device_type := DEVICE_TYPES.get(nd_type): return device_type logging.warning('Unknown device type: {}'.format(nd_type)) return None @property def tunnel_mode(self) -> str: if self.type == 'tunnel' and self.iproute_type: return self.iproute_type return None @property def backend(self) -> str: if (self.nd and 'unmanaged' not in self.nd.get('SetupState', '') and 'run/systemd/network/10-netplan-' in self.nd.get('NetworkFile', '')): return 'networkd' elif self.nm and 'run/NetworkManager/system-connections/netplan-' in self.nm.get('filename', ''): return 'NetworkManager' return None @property def netdef_id(self) -> str: if self.backend == 'networkd': return self.nd.get('NetworkFile', '').split( 'run/systemd/network/10-netplan-')[1].split('.network')[0] elif self.backend == 'NetworkManager': netdef = self.nm.get('filename', '').split( 'run/NetworkManager/system-connections/netplan-')[1].split('.nmconnection')[0] if self.nm.get('type', '') == '802-11-wireless': ssid = self.query_nm_ssid(self.nm.get('name')) if ssid: # XXX: escaping needed? netdef = netdef.split('-' + ssid)[0] return netdef return None @property def vendor(self) -> str: if self.nd and 'Vendor' in self.nd and self.nd['Vendor']: return self.nd['Vendor'].strip() return None @property def ssid(self) -> str: if self.type == 'wifi': # XXX: available from networkctl's JSON output as of v250: # https://github.com/systemd/systemd/commit/da7c995 for line in self._networkctl.splitlines(): line = line.strip() key = 'WiFi access point: ' if line.startswith(key): ssid = line[len(key):-len(' (xB:SS:ID:xx:xx:xx)')].strip() return ssid if ssid else None return None @property def activation_mode(self) -> str: if self.backend == 'networkd': # XXX: available from networkctl's JSON output as of v250: # https://github.com/systemd/systemd/commit/3b60ede for line in self._networkctl.splitlines(): line = line.strip() key = 'Activation Policy: ' if line.startswith(key): mode = line[len(key):].strip() return mode if mode != 'up' else None # XXX: this is not fully supported on NetworkManager, only 'manual'/'up' elif self.backend == 'NetworkManager': return 'manual' if self.nm['autoconnect'] == 'no' else None return None class SystemConfigState(): ''' Collects the system's network configuration ''' def __init__(self, ifname=None, all=False): # Make sure sd-networkd is running, as we need the data it provides. if not utils.systemctl_is_active('systemd-networkd.service'): if utils.systemctl_is_masked('systemd-networkd.service'): logging.error('\'netplan status\' depends on networkd, ' 'but systemd-networkd.service is masked. ' 'Please start it.') sys.exit(1) logging.debug('systemd-networkd.service is not active. Starting...') utils.systemctl('start', ['systemd-networkd.service'], True) # required data: iproute2 and sd-networkd can be expected to exist, # due to hard package dependencies iproute2 = self.query_iproute2() networkd = self.query_networkd() if not iproute2 or not networkd: logging.error('Could not query iproute2 or systemd-networkd') sys.exit(1) # optional data nmcli = self.query_nm() route4, route6 = self.query_routes() dns_addresses, dns_search = self.query_resolved() self.interface_list = [Interface(itf, networkd, nmcli, (dns_addresses, dns_search), (route4, route6)) for itf in iproute2] # get bridge/bond/vrf data self.correlate_members_and_uplink(self.interface_list) # show only active interfaces by default filtered = [itf for itf in self.interface_list if itf.operstate != 'DOWN'] # down interfaces do not contribute anything to the online state online_state = self.query_online_state(filtered) # show only a single interface, if requested # XXX: bash completion (for interfaces names) if ifname: filtered = [next((itf for itf in self.interface_list if itf.name == ifname), None)] filtered = [elem for elem in filtered if elem is not None] if ifname and filtered == []: logging.error('Could not find interface {}'.format(ifname)) sys.exit(1) # Global state self.state = { 'netplan-global-state': { 'online': online_state, 'nameservers': self.resolvconf_json() } } # Per interface itf_iter = self.interface_list if all else filtered for itf in itf_iter: ifname, obj = itf.json() self.state[ifname] = obj @classmethod def resolvconf_json(cls) -> dict: res = { 'addresses': [], 'search': [], 'mode': None, } try: with open('/etc/resolv.conf') as f: # check first line for systemd-resolved stub or compat modes firstline = f.readline() if '# This is /run/systemd/resolve/stub-resolv.conf' in firstline: res['mode'] = 'stub' elif '# This is /run/systemd/resolve/resolv.conf' in firstline: res['mode'] = 'compat' for line in [firstline] + f.readlines(): if line.startswith('nameserver'): res['addresses'] += line.split()[1:] # append if line.startswith('search'): res['search'] = line.split()[1:] # override except Exception as e: logging.warning('Cannot parse /etc/resolv.conf: {}'.format(str(e))) return res @classmethod def query_online_state(cls, interfaces: list) -> bool: # TODO: fully implement network-online.target specification (FO020): # https://discourse.ubuntu.com/t/spec-definition-of-an-online-system/27838 for itf in interfaces: if itf.up and itf.addresses and itf.routes and itf.dns_addresses: non_local_ips = [] for addr in itf.addresses: ip, extra = list(addr.items())[0] if 'flags' not in extra or 'link' not in extra['flags']: non_local_ips.append(ip) default_routes = [x for x in itf.routes if x.get('to', None) == 'default'] if non_local_ips and default_routes and itf.dns_addresses: return True return False @classmethod def process_generic(cls, cmd_output: str) -> JSON: return json.loads(cmd_output) @classmethod def query_iproute2(cls) -> JSON: data: JSON = None try: output: str = subprocess.check_output(['ip', '-d', '-j', 'addr'], text=True) data = cls.process_generic(output) except Exception as e: logging.critical('Cannot query iproute2 interface data: {}'.format(str(e))) return data @classmethod def process_networkd(cls, cmd_output) -> JSON: return json.loads(cmd_output)['Interfaces'] @classmethod def query_networkd(cls) -> JSON: data: JSON = None try: output: str = subprocess.check_output(['networkctl', '--json=short'], text=True) data = cls.process_networkd(output) except Exception as e: logging.critical('Cannot query networkd interface data: {}'.format(str(e))) return data @classmethod def process_nm(cls, cmd_output) -> JSON: data: JSON = [] for line in cmd_output.splitlines(): split = line.split(':') dev = split[0] if split[0] else None if dev: # ignore inactive connection profiles data.append({ 'device': dev, 'name': split[1], 'uuid': split[2], 'filename': split[3], 'type': split[4], 'autoconnect': split[5], }) return data @classmethod def query_nm(cls) -> JSON: data: JSON = None try: output: str = utils.nmcli_out(['-t', '-f', 'DEVICE,NAME,UUID,FILENAME,TYPE,AUTOCONNECT', 'con', 'show']) data = cls.process_nm(output) except Exception as e: logging.debug('Cannot query NetworkManager interface data: {}'.format(str(e))) return data @classmethod def query_routes(cls) -> tuple: data4 = None data6 = None try: output4: str = subprocess.check_output(['ip', '-d', '-j', '-4', 'route', 'show', 'table', 'all'], text=True) data4: JSON = cls.process_generic(output4) output6: str = subprocess.check_output(['ip', '-d', '-j', '-6', 'route', 'show', 'table', 'all'], text=True) data6: JSON = cls.process_generic(output6) except Exception as e: logging.debug('Cannot query iproute2 route data: {}'.format(str(e))) # Add the address family to the data # IPv4: 2, IPv6: 10 if data4: for route in data4: route.update({'family': socket.AF_INET.value}) if data6: for route in data6: route.update({'family': socket.AF_INET6.value}) return (data4, data6) @classmethod def query_resolved(cls) -> tuple: addresses = None search = None try: ipc = dbus.SystemBus() resolve1 = ipc.get_object('org.freedesktop.resolve1', '/org/freedesktop/resolve1') resolve1_if = dbus.Interface(resolve1, 'org.freedesktop.DBus.Properties') res = resolve1_if.GetAll('org.freedesktop.resolve1.Manager') addresses = res['DNS'] search = res['Domains'] except Exception as e: logging.debug('Cannot query resolved DNS data: {}'.format(str(e))) return (addresses, search) @classmethod def query_members(cls, ifname: str) -> List[str]: ''' Return a list containing the interfaces that are members of a bond/bridge/vrf ''' members = [] output: str = None try: output = subprocess.check_output( ['ip', '-d', '-j', 'link', 'show', 'master', ifname], text=True) # wokeignore:rule=master except Exception as e: logging.warning('Cannot query bridge: {}'.format(str(e))) return [] output_json = json.loads(output) for member in output_json: members.append(member.get('ifname')) return members @classmethod def correlate_members_and_uplink(cls, interfaces: List[Interface]) -> None: ''' Associate interfaces with their members and parent interfaces. If an interface is a member of a bond/bridge/vrf, identify which interface if a member of. If an interface has members, identify what are the members. ''' uplink_types = ['bond', 'bridge', 'vrf'] members_to_uplink = {} uplink_to_members = defaultdict(list) for interface in filter(lambda i: i.type in uplink_types, interfaces): members = cls.query_members(interface.name) for member in members: member_tuple = namedtuple('Member', ['name', 'type']) members_to_uplink[member] = member_tuple(interface.name, interface.type) uplink_to_members[interface.name] = members for interface in interfaces: if uplink := members_to_uplink.get(interface.name): if uplink.type == 'bridge': interface.bridge = uplink.name if uplink.type == 'bond': interface.bond = uplink.name if uplink.type == 'vrf': interface.vrf = uplink.name if interface.type in uplink_types: if members := uplink_to_members.get(interface.name): interface.members = members @property def number_of_interfaces(self) -> int: return len(self.interface_list) def get_data(self) -> dict: return self.state class NetplanConfigState(): ''' Collects the Netplan's network configuration ''' def __init__(self, subtree='all', rootdir='/'): parser = netplan.Parser() parser.load_yaml_hierarchy(rootdir) np_state = netplan.State() np_state.import_parser_results(parser) self.netdefs = np_state.netdefs self.state = StringIO() if subtree == 'all': np_state._dump_yaml(output_file=self.state) else: if not subtree.startswith('network'): subtree = '.'.join(('network', subtree)) # Split at '.' but not at '\.' via negative lookbehind expression subtree = re.split(r'(? str: return self.state.getvalue() def get_data(self) -> dict: return yaml.safe_load(self.state.getvalue()) netplan-1.0/netplan_cli/cli/state_diff.py000066400000000000000000000704421457004145200205450ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2023 Canonical, Ltd. # Authors: Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from collections import defaultdict import ipaddress import json from typing import AbstractSet from netplan.netdef import NetplanRoute from netplan_cli.cli.state import SystemConfigState, NetplanConfigState, DEVICE_TYPES from netplan_cli.cli.utils import is_valid_macaddress, route_table_lookup class DiffJSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, NetplanRoute): return obj.to_dict() # Shouldn't be reached as the only non-serializable type we have at the moment is NetplanRoute return json.JSONEncoder.default(self, obj) # pragma: nocover (only NetplanRoute requires the encoder) class NetplanDiffState(): ''' DiffState is mainly responsible for getting both system's and Netplan's configuration state, compare them and provide a data-structure containing the differences it found. ''' def __init__(self, system_state: SystemConfigState, netplan_state: NetplanConfigState): self.system_state = system_state self.netplan_state = netplan_state self.route_lookup_table_names = {} def get_full_state(self) -> dict: ''' Return the states of both the system and Netplan in a common representation that makes it easier to compare them. ''' full_state = { 'interfaces': {} } system_interfaces = self._get_system_interfaces() netplan_interfaces = self._get_netplan_interfaces() # Merge all the interfaces in the same data structure all_interfaces = set(list(system_interfaces.keys()) + list(netplan_interfaces.keys())) for interface in all_interfaces: full_state['interfaces'][interface] = {} for interface, config in system_interfaces.items(): full_state['interfaces'][interface].update(config) for interface, config in netplan_interfaces.items(): full_state['interfaces'][interface].update(config) return full_state def get_diff(self, interface: str = '') -> dict: ''' Compare the configuration of interfaces currently found in the system against Netplan configuration. A number of heuristics are used to eliminate configuration that is automatically set in the system, such as certain routes and IP addresses. That is necessary because this configuration will not be found in Netplan. For example, if Netplan is enabling DHCP on an interface and not defining any extra IP addresses, we don't count the IPs automatically assigned to the interface as a difference. We do though count the eventual absence of addresses that should be assigned by DHCP as a difference. ''' full_state = self.get_full_state() interfaces = self._get_comparable_interfaces(full_state.get('interfaces', {})) if interface: if config := interfaces.get(interface): interfaces = {interface: config} else: interfaces = {} report = self._create_new_report() self._analyze_missing_interfaces(report, interface) for interface, config in interfaces.items(): netdef_id = config.get('system_state', {}).get('id') index = config.get('system_state', {}).get('index') iface = self._create_new_iface(netdef_id, interface, index) self._analyze_ip_addresses(config, iface) self._analyze_nameservers(config, iface) self._analyze_search_domains(config, iface) self._analyze_mac_addresses(config, iface) self._analyze_routes(config, iface) self._analyze_parent_links(config, iface) report['interfaces'].update(iface) # Sort the list of interfaces according to their indices. report['interfaces'] = dict(sorted(report['interfaces'].items(), key=lambda iface: iface[1].get('index'))) return report def _create_new_report(self) -> dict: return { 'interfaces': {}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {}, } def _create_new_iface(self, netdef_id: str, interface: str, index: int) -> dict: return { interface: { 'index': index, 'name': interface, 'id': netdef_id, 'system_state': {}, 'netplan_state': {}, } } def _analyze_ip_addresses(self, config: dict, iface: dict) -> None: name = list(iface.keys())[0] netplan_ips = {ip for ip in config.get('netplan_state', {}).get('addresses', [])} netplan_ips = self._normalize_ip_addresses(netplan_ips) missing_dhcp4_address = config.get('netplan_state', {}).get('dhcp4', False) missing_dhcp6_address = config.get('netplan_state', {}).get('dhcp6', False) link_local = config.get('netplan_state', {}).get('link_local', []) system_ips = set() for addr, addr_data in config.get('system_state', {}).get('addresses', {}).items(): ip = ipaddress.ip_interface(addr) flags = addr_data.get('flags', []) # Select only static IPs if 'dhcp' not in flags and 'link' not in flags: system_ips.add(addr) # Handle the link local address # If it's present but the respective setting is not enabled in the netdef # it's considered a difference. if 'link' in flags and ip.is_link_local: if isinstance(ip.ip, ipaddress.IPv4Address) and 'ipv4' not in link_local: system_ips.add(addr) if isinstance(ip.ip, ipaddress.IPv6Address) and 'ipv6' not in link_local: system_ips.add(addr) # TODO: improve the detection of addresses assigned dynamically # in the class Interface. if 'dhcp' in flags: if isinstance(ip.ip, ipaddress.IPv4Address): missing_dhcp4_address = False if isinstance(ip.ip, ipaddress.IPv6Address): missing_dhcp6_address = False present_only_in_netplan = netplan_ips.difference(system_ips) present_only_in_system = system_ips.difference(netplan_ips) if missing_dhcp4_address: iface[name]['system_state']['missing_dhcp4_address'] = True if missing_dhcp6_address: iface[name]['system_state']['missing_dhcp6_address'] = True if present_only_in_system: iface[name]['netplan_state'].update({ 'missing_addresses': list(sorted(present_only_in_system)), }) if present_only_in_netplan: iface[name]['system_state'].update({ 'missing_addresses': list(sorted(present_only_in_netplan)), }) def _get_comparable_interfaces(self, interfaces: dict) -> dict: ''' In order to compare interfaces, they must exist in the system AND in Netplan. Here we filter out interfaces that don't have a system_state, a netplan_state or a netdef ID. There is a special case where the interface will have a system_state and a netdef_id but will be missing in Netplan. That will happen when the user removes the interface only from Netplan but doesn't run netplan apply. ''' filtered = {} for interface, config in interfaces.items(): if config.get('system_state') is None or config.get('netplan_state') is None: continue if not config.get('system_state', {}).get('id'): continue filtered[interface] = config return filtered def _normalize_ip_addresses(self, addresses: set) -> set: ''' Apply some transformations to IP addresses so their representation will match the system's. ''' new_ips_set = set() for ip in addresses: ip = self._compress_ipv6_address(ip) new_ips_set.add(ip) return new_ips_set def _compress_ipv6_address(self, address: str) -> str: ''' Compress IPv6 addresses to match the system representation Example: 1:2:0:0::123/64 -> 1:2::123/64 1:2:0:0::123 -> 1:2::123 If "address" is not an IPv6Address, return the original value ''' try: addr = ipaddress.ip_interface(address) if '/' in address: return addr.with_prefixlen return str(addr.ip) except ValueError: return address def _analyze_nameservers(self, config: dict, iface: dict) -> None: name = list(iface.keys())[0] # TODO: improve analysis of configuration received from DHCP netplan_nameservers = set(config.get('netplan_state', {}).get('nameservers_addresses', [])) system_nameservers = set(config.get('system_state', {}).get('nameservers_addresses', [])) # Filter out dynamically assigned DNS data # Here we implement some heuristics to try to filter out dynamic DNS configuration # # If the nameserver address is the same as a RA route we assume it's dynamic system_routes = config.get('system_state', {}).get('routes', []) ra_routes = [r.via for r in system_routes if r.protocol == 'ra' and r.via] system_nameservers = {ns for ns in system_nameservers if ns not in ra_routes} # If the netplan configuration has DHCP enabled and an empty list of nameservers # we assume it's dynamic. # Note: Some useful information can be found in /var/run/systemd/netif/leases/ # but the lease files have a comment saying they shouldn't be parsed. # There is a feature request to expose more DHCP information via the DBus API # https://github.com/systemd/systemd/issues/27699 if not netplan_nameservers: if config.get('netplan_state', {}).get('dhcp4'): system_nameservers = {ns for ns in system_nameservers if not isinstance(ipaddress.ip_address(ns), ipaddress.IPv4Address)} if config.get('netplan_state', {}).get('dhcp6'): system_nameservers = {ns for ns in system_nameservers if not isinstance(ipaddress.ip_address(ns), ipaddress.IPv6Address)} present_only_in_netplan = netplan_nameservers.difference(system_nameservers) present_only_in_system = system_nameservers.difference(netplan_nameservers) if present_only_in_system: iface[name]['netplan_state'].update({ 'missing_nameservers_addresses': list(present_only_in_system), }) if present_only_in_netplan: iface[name]['system_state'].update({ 'missing_nameservers_addresses': list(present_only_in_netplan), }) def _analyze_search_domains(self, config: dict, iface: dict) -> None: name = list(iface.keys())[0] netplan_search_domains = set(config.get('netplan_state', {}).get('nameservers_search', [])) system_search_domains = set(config.get('system_state', {}).get('nameservers_search', [])) # If the netplan configuration has DHCP enabled and an empty list of search domains # we assume it's dynamic if not netplan_search_domains: if config.get('netplan_state', {}).get('dhcp4') or config.get('netplan_state', {}).get('dhcp6'): system_search_domains = set() present_only_in_netplan = netplan_search_domains.difference(system_search_domains) present_only_in_system = system_search_domains.difference(netplan_search_domains) if present_only_in_system: iface[name]['netplan_state'].update({ 'missing_nameservers_search': list(present_only_in_system), }) if present_only_in_netplan: iface[name]['system_state'].update({ 'missing_nameservers_search': list(present_only_in_netplan), }) def _analyze_mac_addresses(self, config: dict, iface: dict) -> None: name = list(iface.keys())[0] system_macaddress = config.get('system_state', {}).get('macaddress') netplan_macaddress = config.get('netplan_state', {}).get('macaddress') # if the macaddress in netplan is an special option (such as 'random') # don't try to diff it against the system MAC address if netplan_macaddress and not is_valid_macaddress(netplan_macaddress): return if system_macaddress and netplan_macaddress: if system_macaddress != netplan_macaddress: iface[name]['system_state'].update({ 'missing_macaddress': netplan_macaddress }) iface[name]['netplan_state'].update({ 'missing_macaddress': system_macaddress }) def _analyze_routes(self, config: dict, iface: dict) -> None: name = list(iface.keys())[0] netplan_routes = set(config.get('netplan_state', {}).get('routes', [])) system_routes = set(config.get('system_state', {}).get('routes', [])) netplan_routes = self._normalize_routes(netplan_routes) # Filter out some routes that are expected to be added automatically system_addresses = [ip for ip in config.get('system_state', {}).get('addresses', {})] system_routes = self._filter_system_routes(system_routes, system_addresses, config) present_only_in_netplan = netplan_routes.difference(system_routes) present_only_in_system = system_routes.difference(netplan_routes) if present_only_in_system: iface[name]['netplan_state'].update({ 'missing_routes': [route for route in sorted(present_only_in_system, key=lambda r: r.to)], }) if present_only_in_netplan: iface[name]['system_state'].update({ 'missing_routes': [route for route in sorted(present_only_in_netplan, key=lambda r: r.to)], }) def _analyze_missing_interfaces(self, report: dict, interface: str) -> None: netplan_interfaces = {iface for iface in self.netplan_state.netdefs} system_interfaces_netdef_ids = {iface.netdef_id for iface in self.system_state.interface_list if iface.netdef_id} netplan_only = netplan_interfaces.difference(system_interfaces_netdef_ids) # Filtering out disconnected wifi netdefs # If a wifi netdef is present in the netplan_only list it's because it's disconnected netplan_only = list(filter(lambda i: self.netplan_state.netdefs.get(i).type != 'wifis', netplan_only)) system_only = [] for iface in self.system_state.interface_list: if iface.netdef_id not in netplan_interfaces: system_only.append(iface.name) netplan_only = sorted(netplan_only) system_only = sorted(system_only) if interface: netplan_only = filter(lambda i: i == interface, netplan_only) system_only = filter(lambda i: i == interface, system_only) system_state = self.system_state.get_data() for iface in netplan_only: iface_type = self.netplan_state.netdefs.get(iface).type report['missing_interfaces_system'][iface] = { 'type': DEVICE_TYPES.get(iface_type, 'other') } for iface in system_only: report['missing_interfaces_netplan'][iface] = { 'type': system_state.get(iface).get('type', 'other'), 'index': system_state.get(iface).get('index'), } def _analyze_parent_links(self, config: dict, iface: dict) -> None: ''' Analyze if interfaces such as bonds, bridges and VRFs are correctly attached to their members and vice versa. ''' name = list(iface.keys())[0] bond = [config.get('system_state', {}).get('bond'), config.get('netplan_state', {}).get('bond')] bridge = [config.get('system_state', {}).get('bridge'), config.get('netplan_state', {}).get('bridge')] vrf = [config.get('system_state', {}).get('vrf'), config.get('netplan_state', {}).get('vrf')] interfaces = [config.get('system_state', {}).get('interfaces', []), config.get('netplan_state', {}).get('interfaces', [])] if bond != [None, None] and bond[0] != bond[1]: if bond[0]: iface[name]['netplan_state']['missing_bond_link'] = bond[0] if bond[1]: iface[name]['system_state']['missing_bond_link'] = bond[1] if bridge != [None, None] and bridge[0] != bridge[1]: if bridge[0]: iface[name]['netplan_state']['missing_bridge_link'] = bridge[0] if bridge[1]: iface[name]['system_state']['missing_bridge_link'] = bridge[1] if vrf != [None, None] and vrf[0] != vrf[1]: if vrf[0]: iface[name]['netplan_state']['missing_vrf_link'] = vrf[0] if vrf[1]: iface[name]['system_state']['missing_vrf_link'] = vrf[1] if interfaces != [[], []]: system = set(interfaces[0]) netplan = set(interfaces[1]) if system != netplan: if missing_system := netplan - system: iface[name]['system_state']['missing_interfaces'] = list(missing_system) if missing_netplan := system - netplan: iface[name]['netplan_state']['missing_interfaces'] = list(missing_netplan) def _normalize_routes(self, routes: set) -> set: ''' Apply some transformations to Netplan routes so their representation will match the system's. ''' new_routes_set = set() for route in routes: # If the table is unspecified we set it to main if route.table == NetplanRoute._TABLE_UNSPEC_: route.table = self._default_route_tables_name_to_number('main') # If the addresses are IPv6, compress them so it will match the system representation route.to = self._compress_ipv6_address(route.to) route.from_addr = self._compress_ipv6_address(route.from_addr) route.via = self._compress_ipv6_address(route.via) # If the route.to prefix is either /32 and /128 we remove it to match # the system representation: if route.to != 'default': ip_prefix = route.to.split('/') if ip_prefix[1] == '32' or ip_prefix[1] == '128': route.to = ip_prefix[0] new_routes_set.add(route) return new_routes_set def _filter_system_routes(self, system_routes: AbstractSet[NetplanRoute], system_addresses: list[str], config: dict) -> set: ''' Some routes found in the system are installed automatically/dynamically without being configured in Netplan. Here we implement some heuristics to remove these routes from the list we want to compare. We do that because these type of routes will probably never be found in the Netplan configuration so there is no point in comparing them against Netplan. ''' local_networks = [str(ipaddress.ip_interface(ip).network) for ip in system_addresses] # filter out the local link network as we give special treatment to it local_networks = list(filter(lambda n: n != 'fe80::/64', local_networks)) addresses = [str(ipaddress.ip_interface(ip).ip) for ip in system_addresses] link_local = config.get('netplan_state', {}).get('link_local', []) routes = set() for route in system_routes: # Filter out link routes (but not link local as we handle them differently) if route.scope == 'link' and route.to != 'default' and not ipaddress.ip_interface(route.to).is_link_local: continue # Filter out routes installed by DHCP or RA if route.protocol == 'dhcp' or route.protocol == 'ra': continue # Filter out Link Local routes # We only filter them out if the respective 'link-local' setting is present in the netdef if route.to != 'default': route_to = ipaddress.ip_interface(route.to) if route_to.is_link_local: if route.family == 10 and 'ipv6' in link_local: continue if route.family == 2 and 'ipv4' in link_local: continue # Filter out host scoped routes if (route.scope == 'host' and route.type == 'local' and (route.to in addresses or ipaddress.ip_interface(route.to).is_loopback)): continue # Filter out the default IPv6 multicast route if route.family == 10 and route.type == 'multicast' and route.to == 'ff00::/8': continue # Filter IPv6 local routes if route.family == 10 and (route.to in local_networks or route.to in addresses): continue routes.add(route) return routes def _get_netplan_interfaces(self) -> dict: system_interfaces = self.system_state.get_data() interfaces = {} for interface, config in self.netplan_state.netdefs.items(): iface = {} iface[interface] = {'netplan_state': {'id': interface}} iface_ref = iface[interface]['netplan_state'] iface_ref['type'] = DEVICE_TYPES.get(config.type, 'other') iface_ref['dhcp4'] = config.dhcp4 iface_ref['dhcp6'] = config.dhcp6 iface_ref['link_local'] = config.link_local addresses = [addr for addr in config.addresses] if addresses: iface_ref['addresses'] = {} for addr in addresses: flags = {} if addr.label: flags['label'] = addr.label if addr.lifetime: flags['lifetime'] = addr.lifetime iface_ref['addresses'][str(addr)] = {'flags': flags} if nameservers := list(config.nameserver_addresses): iface_ref['nameservers_addresses'] = nameservers if search := list(config.nameserver_search): iface_ref['nameservers_search'] = search if routes := list(config.routes): iface_ref['routes'] = routes if mac := config.macaddress: iface_ref['macaddress'] = mac if bridge := config.links.get('bridge'): iface_ref['bridge'] = bridge.id if bond := config.links.get('bond'): iface_ref['bond'] = bond.id if vrf := config.links.get('vrf'): iface_ref['vrf'] = vrf.id if interface not in system_interfaces: # If the netdef ID doesn't correspond to any interface name in the system, # it might be associated with multiple system interfaces, such as when the 'match' key is used, # or the interface name is set in the passthrough section, such as when we create a connection via # Network Manager and the netdef ID is the UUID of the connetion. # In these cases, we need to look for all the system's interfaces # pointing to this netdef and add one netdef entry per device. found_some = False for key, value in system_interfaces.items(): if netdef_id := value.get('id'): if netdef_id == interface: found_some = True interfaces[key] = iface[interface] # If we don't find any system interface associated with the netdef # that's because it's not matching any device. In this case, we add the # netdef ID to the list anyway. if not found_some: interfaces.update(iface) else: interfaces.update(iface) self._netplan_state_find_parents(interfaces) return interfaces def _netplan_state_find_parents(self, interfaces: dict) -> None: ''' Associates interfaces with their parents ''' parents = defaultdict(set) for interface, config in interfaces.items(): if link := config['netplan_state'].get('bridge'): parents[link].add(interface) if link := config['netplan_state'].get('bond'): parents[link].add(interface) if link := config['netplan_state'].get('vrf'): parents[link].add(interface) for interface, members in parents.items(): interfaces[interface]['netplan_state']['interfaces'] = list(members) def _get_system_interfaces(self) -> dict: interfaces = {} for interface, config in self.system_state.get_data().items(): if interface == 'netplan-global-state': continue device_type = config.get('type') interfaces[interface] = {'system_state': {'type': device_type}} if netdef_id := config.get('id'): interfaces[interface]['system_state']['id'] = netdef_id iface_ref = interfaces[interface]['system_state'] if index := config.get('index'): iface_ref['index'] = index addresses = {} for addr in config.get('addresses', []): ip = list(addr.keys())[0] prefix = addr.get(ip).get('prefix') full_addr = f'{ip}/{prefix}' addresses[full_addr] = {'flags': addr.get(ip).get('flags', [])} if addresses: iface_ref['addresses'] = addresses if nameservers := config.get('dns_addresses'): iface_ref['nameservers_addresses'] = nameservers if search := config.get('dns_search'): iface_ref['nameservers_search'] = search if routes := config.get('routes'): iface_ref['routes'] = [self._system_route_to_netplan(route) for route in routes] if mac := config.get('macaddress'): iface_ref['macaddress'] = mac if uplink_interfaces := config.get('interfaces'): iface_ref['interfaces'] = uplink_interfaces if bond := config.get('bond'): iface_ref['bond'] = bond if bridge := config.get('bridge'): iface_ref['bridge'] = bridge if vrf := config.get('vrf'): iface_ref['vrf'] = vrf return interfaces def _system_route_to_netplan(self, system_route: dict) -> NetplanRoute: route = {} if family := system_route.get('family'): route['family'] = family if to := system_route.get('to'): route['to'] = to if via := system_route.get('via'): route['via'] = via if from_addr := system_route.get('from'): route['from_addr'] = from_addr if metric := system_route.get('metric'): route['metric'] = metric if scope := system_route.get('scope'): route['scope'] = scope if route_type := system_route.get('type'): route['type'] = route_type if protocol := system_route.get('protocol'): route['protocol'] = protocol if table := system_route.get('table'): route['table'] = self._default_route_tables_name_to_number(table) return NetplanRoute(**route) def _default_route_tables_name_to_number(self, name: str) -> int: if name.isdigit(): return int(name) if not self.route_lookup_table_names: self.route_lookup_table_names = route_table_lookup() return self.route_lookup_table_names.get(name, 0) netplan-1.0/netplan_cli/cli/utils.py000066400000000000000000000271061457004145200175740ustar00rootroot00000000000000# Copyright (C) 2018-2020 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Łukasz 'sil2100' Zemczak # Author: Lukas 'slyon' Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import os import logging import argparse import subprocess import netifaces import fnmatch import re from ..configmanager import ConfigurationError from netplan import NetDefinition, NetplanException NM_SERVICE_NAME = 'NetworkManager.service' NM_SNAP_SERVICE_NAME = 'snap.network-manager.networkmanager.service' OLD_RT_TABLES_PATH = '/etc/iproute2/rt_tables' NEW_RT_TABLES_PATH = '/usr/share/iproute2/rt_tables' RT_TABLES_DEFAULT = {0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} config_errors = (ConfigurationError, NetplanException, RuntimeError) def get_generator_path(): return os.environ.get('NETPLAN_GENERATE_PATH', '/usr/libexec/netplan/generate') def is_nm_snap_enabled(): return subprocess.call(['systemctl', '--quiet', 'is-enabled', NM_SNAP_SERVICE_NAME], stderr=subprocess.DEVNULL) == 0 def nmcli(args): # pragma: nocover (covered in autopkgtest) # 'nmcli' could be /usr/bin/nmcli or /snap/bin/nmcli -> /snap/bin/network-manager.nmcli # PATH is defined in cli/core.py subprocess.check_call(['nmcli'] + args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def nmcli_out(args: list) -> str: # pragma: nocover (covered in autopkgtest) # 'nmcli' could be /usr/bin/nmcli or /snap/bin/nmcli -> /snap/bin/network-manager.nmcli # PATH is defined in cli/core.py return subprocess.check_output(['nmcli'] + args, text=True) def nm_running(): # pragma: nocover (covered in autopkgtest) '''Check if NetworkManager is running''' try: nmcli(['general']) return True except (OSError, subprocess.SubprocessError): return False def nm_interfaces(paths, devices): pat = re.compile('^interface-name=(.*)$') interfaces = set() for path in paths: with open(path, 'r') as f: for line in f: m = pat.match(line) if m: # Expand/match globbing of interface names, to real devices interfaces.update(set(fnmatch.filter(devices, m.group(1)))) break # skip to next file return interfaces def nm_get_connection_for_interface(interface: str) -> str: output = nmcli_out(['-m', 'tabular', '-f', 'GENERAL.CONNECTION', 'device', 'show', interface]) lines = output.strip().split('\n') connection = lines[1] return connection if connection != '--' else '' def nm_bring_interface_up(connection: str) -> None: # pragma: nocover (must be covered by NM autopkgtests) try: nmcli(['connection', 'up', connection]) except subprocess.CalledProcessError: pass def systemctl_network_manager(action, sync=False): # If the network-manager snap is installed use its service # name rather than the one of the deb packaged NetworkManager if is_nm_snap_enabled(): return systemctl(action, [NM_SNAP_SERVICE_NAME], sync) return systemctl(action, [NM_SERVICE_NAME], sync) # pragma: nocover (covered in autopkgtest) def systemctl(action: str, services: list, sync: bool = False): if len(services) >= 1: command = ['systemctl', action] if not sync: command.append('--no-block') command.extend(services) subprocess.check_call(command) def networkd_interfaces(): interfaces = set() out = subprocess.check_output(['networkctl', '--no-pager', '--no-legend'], text=True) for line in out.splitlines(): s = line.strip().split(' ') if s[0].isnumeric() and s[-1] not in ['unmanaged', 'linger']: interfaces.add(s[0]) return interfaces def networkctl_reload(): subprocess.check_call(['networkctl', 'reload']) def networkctl_reconfigure(interfaces): if len(interfaces) >= 1: subprocess.check_call(['networkctl', 'reconfigure'] + list(interfaces)) def systemctl_is_active(unit_pattern): '''Return True if at least one matching unit is running''' if subprocess.call(['systemctl', '--quiet', 'is-active', unit_pattern]) == 0: return True return False def systemctl_is_masked(unit_pattern): '''Return True if output is "masked" or "masked-runtime"''' res = subprocess.run(['systemctl', 'is-enabled', unit_pattern], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if res.returncode > 0 and 'masked' in res.stdout: return True return False def systemctl_is_installed(unit_pattern): '''Return True if returncode is other than "not-found" (4)''' res = subprocess.run(['systemctl', 'is-enabled', unit_pattern], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if res.returncode != 4: return True return False def systemctl_daemon_reload(): '''Reload systemd unit files from disk and re-calculate its dependencies''' subprocess.check_call(['systemctl', 'daemon-reload']) def ip_addr_flush(iface): '''Flush all IP addresses of a given interface via iproute2''' subprocess.check_call(['ip', 'addr', 'flush', iface], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def get_interface_driver_name(interface, only_down=False): # pragma: nocover (covered in autopkgtest) devdir = os.path.join('/sys/class/net', interface) if only_down: try: with open(os.path.join(devdir, 'operstate')) as f: state = f.read().strip() if state != 'down': logging.debug('device %s operstate is %s, not changing', interface, state) return None except IOError as e: logging.error('Cannot determine operstate of %s: %s', interface, str(e)) return None try: driver = os.path.realpath(os.path.join(devdir, 'device', 'driver')) driver_name = os.path.basename(driver) except IOError as e: logging.debug('Cannot replug %s: cannot read link %s/device: %s', interface, devdir, str(e)) return None return driver_name def get_interface_macaddress(interface): # return an empty list (and string) if no LL data can be found link = netifaces.ifaddresses(interface).get(netifaces.AF_LINK, [{}])[0] return link.get('addr', '') def find_matching_iface(interfaces: list, netdef): assert isinstance(netdef, NetDefinition) assert netdef._has_match matches = list(filter(lambda itf: netdef._match_interface( iface_name=itf, iface_driver=get_interface_driver_name(itf), iface_mac=get_interface_macaddress(itf)), interfaces)) # Return current name of unique matched interface, if available if len(matches) != 1: logging.info(matches) return None return matches[0] def is_valid_macaddress(macaddress: str) -> bool: MAC_PATTERN = '^[a-fA-F0-9][a-fA-F0-9](:[a-fA-F0-9][a-fA-F0-9]){5}((:[a-fA-F0-9][a-fA-F0-9]){14})?$' return re.match(MAC_PATTERN, macaddress) is not None def route_table_lookup() -> dict: lookup_table = {} path = NEW_RT_TABLES_PATH if not os.path.exists(path): path = OLD_RT_TABLES_PATH try: with open(path, 'r') as rt_tables: for line in rt_tables: split_line = line.split() if len(split_line) == 2 and split_line[0].isnumeric(): lookup_table[int(split_line[0])] = split_line[1] lookup_table[split_line[1]] = int(split_line[0]) except Exception: logging.debug(f'Cannot open \'{path}\' for reading') # defaults to the standard content found in the file return RT_TABLES_DEFAULT return lookup_table class NetplanCommand(argparse.Namespace): def __init__(self, command_id, description, leaf=True, testing=False): self.command_id = command_id self.description = description self.leaf_command = leaf self.testing = testing self._args = None self.debug = False self.breakpoint = False self.commandclass = None self.subcommands = {} self.subcommand = None self.func = None self.parser = argparse.ArgumentParser(prog="%s %s" % (sys.argv[0], command_id), description=description, add_help=True) self.parser.add_argument('--debug', action='store_true', help='Enable debug messages') self.parser.add_argument('--breakpoint', action='store_true', help=argparse.SUPPRESS) if not leaf: self.subparsers = self.parser.add_subparsers(title='Available commands', metavar='', dest='subcommand') p_help = self.subparsers.add_parser('help', description='Show this help message', help='Show this help message') p_help.set_defaults(func=self.print_usage) def update(self, args): self._args = args def parse_args(self): ns, self._args = self.parser.parse_known_args(args=self._args, namespace=self) if not self.subcommand and not self.leaf_command: print('You need to specify a command', file=sys.stderr) self.print_usage() def run_command(self): if self.commandclass: self.commandclass.update(self._args) # TODO: (cyphermox) this is actually testable in tests/cli.py; add it. if self.leaf_command and 'help' in self._args: # pragma: nocover (covered in autopkgtest) self.print_usage() if self.breakpoint: # pragma: nocover (cannot be automatically tested) breakpoint() self.func() def print_usage(self): self.parser.print_help(file=sys.stderr) sys.exit(os.EX_USAGE) def _add_subparser_from_class(self, name, commandclass): instance = commandclass() self.subcommands[name] = {} self.subcommands[name]['class'] = name self.subcommands[name]['instance'] = instance if instance.testing: if not os.environ.get('ENABLE_TEST_COMMANDS', None): return p = self.subparsers.add_parser(instance.command_id, description=instance.description, help=instance.description, add_help=False) p.set_defaults(func=instance.run, commandclass=instance) self.subcommands[name]['parser'] = p def _import_subcommands(self, submodules): import inspect for name, obj in inspect.getmembers(submodules): if inspect.isclass(obj) and issubclass(obj, NetplanCommand): self._add_subparser_from_class(name, obj) netplan-1.0/netplan_cli/configmanager.py000066400000000000000000000146721457004145200204710ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan configuration manager''' import logging import netplan import os import shutil import sys import tempfile from typing import Optional class ConfigManager(object): def __init__(self, prefix="/", extra_files={}): self.prefix = prefix self.tempdir = tempfile.mkdtemp(prefix='netplan_') self.temp_etc = os.path.join(self.tempdir, "etc") self.temp_run = os.path.join(self.tempdir, "run") self.extra_files = extra_files self.new_interfaces = set() self.np_state: Optional[netplan.State] = None def __getattr__(self, attr): assert self.np_state is not None, "Must call parse() before accessing the config." return getattr(self.np_state, attr) @property def physical_interfaces(self): assert self.np_state is not None, "Must call parse() before accessing the config." interfaces = {} interfaces.update(self.np_state.ethernets) interfaces.update(self.np_state.modems) interfaces.update(self.np_state.wifis) return interfaces @property def virtual_interfaces(self): assert self.np_state is not None, "Must call parse() before accessing the config." interfaces = {} # what about ovs_ports? interfaces.update(self.np_state.bridges) interfaces.update(self.np_state.bonds) interfaces.update(self.np_state.dummy_devices) interfaces.update(self.np_state.tunnels) interfaces.update(self.np_state.virtual_ethernets) interfaces.update(self.np_state.vlans) interfaces.update(self.np_state.vrfs) return interfaces def parse(self, extra_config=None): """ Parse all our config files to return an object that describes the system's entire configuration, so that it can later be interrogated. Returns a libnetplan State wrapper """ # /run/netplan shadows /etc/netplan/, which shadows /lib/netplan parser = netplan.Parser() try: parser.load_yaml_hierarchy(rootdir=self.prefix) if extra_config: for f in extra_config: parser.load_yaml(f) self.np_state = netplan.State() self.np_state.import_parser_results(parser) except netplan.NetplanException as e: raise ConfigurationError(str(e)) # Convoluted way to dump the parsed config to the logs... with tempfile.TemporaryFile() as tmp: self.np_state._dump_yaml(output_file=tmp) logging.debug("Merged config:\n{}".format(tmp.read())) return self.np_state def add(self, config_dict): for config_file in config_dict: self._copy_file(config_file, config_dict[config_file]) self.extra_files.update(config_dict) # Invalidate the current parsed state self.np_state = None def backup(self, backup_config_dir=True): if backup_config_dir: self._copy_tree(os.path.join(self.prefix, "etc/netplan"), os.path.join(self.temp_etc, "netplan")) self._copy_tree(os.path.join(self.prefix, "run/NetworkManager/system-connections"), os.path.join(self.temp_run, "NetworkManager", "system-connections"), missing_ok=True) self._copy_tree(os.path.join(self.prefix, "run/systemd/network"), os.path.join(self.temp_run, "systemd", "network"), missing_ok=True) def revert(self): try: for extra_file in dict(self.extra_files): os.unlink(self.extra_files[extra_file]) del self.extra_files[extra_file] temp_nm_path = "{}/NetworkManager/system-connections".format(self.temp_run) temp_networkd_path = "{}/systemd/network".format(self.temp_run) if os.path.exists(temp_nm_path): shutil.rmtree(os.path.join(self.prefix, "run/NetworkManager/system-connections")) self._copy_tree(temp_nm_path, os.path.join(self.prefix, "run/NetworkManager/system-connections")) if os.path.exists(temp_networkd_path): shutil.rmtree(os.path.join(self.prefix, "run/systemd/network")) self._copy_tree(temp_networkd_path, os.path.join(self.prefix, "run/systemd/network")) except Exception as e: # pragma: nocover (only relevant to filesystem failures) # If we reach here, we're in big trouble. We may have wiped out # file NM or networkd are using, and we most likely removed the # "new" config -- or at least our copy of it. # Given that we're in some halfway done revert; warn the user # aggressively and drop everything; leaving any remaining backups # around for the user to handle themselves. logging.error("Something really bad happened while reverting config: {}".format(e)) logging.error("You should verify the netplan YAML in /etc/netplan and probably run 'netplan apply' again.") sys.exit(-1) def cleanup(self): shutil.rmtree(self.tempdir) def __del__(self): try: self.cleanup() except FileNotFoundError: # If cleanup() was called before, there is nothing to delete pass def _copy_file(self, src, dst): shutil.copy(src, dst) def _copy_tree(self, src, dst, missing_ok=False): try: shutil.copytree(src, dst) except FileNotFoundError: if missing_ok: pass else: raise class ConfigurationError(Exception): """ Configuration could not be parsed or has otherwise failed to apply """ pass netplan-1.0/netplan_cli/meson.build000066400000000000000000000025321457004145200174510ustar00rootroot00000000000000install_data('../src/netplan.script') install_symlink( 'netplan', pointing_to: '../share/netplan/netplan.script', install_dir: get_option('sbindir')) netplan_module = join_paths(get_option('datadir'), meson.project_name(), 'netplan_cli') features_py = custom_target( build_always_stale: true, output: '_features.py', input: join_paths(meson.project_source_root(), 'features_py_generator.sh'), command: ['sh', '-c', '@INPUT@'], install: true, install_dir: netplan_module, capture: true, ) netplan_sources = files( '__init__.py', 'configmanager.py', 'terminal.py') cli_sources = files( 'cli/__init__.py', 'cli/core.py', 'cli/ovs.py', 'cli/state.py', 'cli/state_diff.py', 'cli/sriov.py', 'cli/utils.py') commands_sources = files( 'cli/commands/__init__.py', 'cli/commands/apply.py', 'cli/commands/generate.py', 'cli/commands/get.py', 'cli/commands/info.py', 'cli/commands/ip.py', 'cli/commands/migrate.py', 'cli/commands/set.py', 'cli/commands/sriov_rebind.py', 'cli/commands/status.py', 'cli/commands/try_command.py') install_data(netplan_sources, install_dir: netplan_module) install_data(cli_sources, install_dir: join_paths(netplan_module, 'cli')) install_data(commands_sources, install_dir: join_paths(netplan_module, 'cli', 'commands')) netplan-1.0/netplan_cli/terminal.py000066400000000000000000000122731457004145200174770ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Terminal / input handling """ import fcntl import os import termios import select import sys class Terminal(object): """ Do minimal terminal mangling to prompt users for input """ def __init__(self, fd): self.fd = fd self.orig_flags = None self.orig_term = None self.save() def enable_echo(self): if sys.stdin.isatty(): attrs = termios.tcgetattr(self.fd) attrs[3] = attrs[3] | termios.ICANON attrs[3] = attrs[3] | termios.ECHO termios.tcsetattr(self.fd, termios.TCSANOW, attrs) def disable_echo(self): if sys.stdin.isatty(): attrs = termios.tcgetattr(self.fd) attrs[3] = attrs[3] & ~termios.ICANON attrs[3] = attrs[3] & ~termios.ECHO termios.tcsetattr(self.fd, termios.TCSANOW, attrs) def enable_nonblocking_io(self): flags = fcntl.fcntl(self.fd, fcntl.F_GETFL) fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) def disable_nonblocking_io(self): flags = fcntl.fcntl(self.fd, fcntl.F_GETFL) fcntl.fcntl(self.fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) def get_confirmation_input(self, timeout=120, message=None): # pragma: nocover (requires user input) """ Get a "confirmation" input from the user, for at most (timeout) seconds. Optionally, customize the message to be displayed. timeout -- timeout to wait for input (default 120) message -- optional customized message ("Press ENTER to (message)") raises: InputAccepted -- the user confirmed the changes InputRejected -- the user rejected the changes """ print("Do you want to keep these settings?\n\n") settings = dict() self.save(settings) self.disable_echo() self.enable_nonblocking_io() if not message: message = "accept the new configuration" print("Press ENTER before the timeout to {}\n\n".format(message)) timeout_now = timeout while (timeout_now > 0): print("Changes will revert in {:>{}} seconds".format(timeout_now, len(str(timeout))), end='\r') # wait at most 1 second for usable input from stdin select.select([sys.stdin], [], [], 1) try: # retrieve any input from the terminal. select() either has # timed out with no input, or found something we can retrieve. c = sys.stdin.read() if (c == '\n'): self.reset(settings) # Yay, user has accepted the changes! raise InputAccepted() except TypeError: # read() above is non-blocking, if there is nothing to read it # will return TypeError, which we should ignore -- on to the # next iteration until timeout. pass timeout_now -= 1 # We reached the timeout for our loop, now revert our change for # non-blocking I/O and signal the caller the changes were essentially # rejected. self.reset(settings) raise InputRejected() def save(self, dest=None): """ Save the terminal's current attributes and flags Optional argument: - dest: if set, save settings to this dict """ orig_flags = fcntl.fcntl(self.fd, fcntl.F_GETFL) orig_term = None if sys.stdin.isatty(): orig_term = termios.tcgetattr(self.fd) if dest is not None: dest.update({'flags': orig_flags, 'term': orig_term}) else: self.orig_flags = orig_flags self.orig_term = orig_term def reset(self, orig=None): """ Reset the terminal to its original attributes and flags Optional argument: - orig: if set, reset to settings from this dict """ orig_term = None orig_flags = None if orig is not None: orig_term = orig.get('term') orig_flags = orig.get('flags') else: orig_term = self.orig_term orig_flags = self.orig_flags if sys.stdin.isatty(): termios.tcsetattr(self.fd, termios.TCSAFLUSH, orig_term) fcntl.fcntl(self.fd, fcntl.F_SETFL, orig_flags) class InputAccepted(Exception): """ Denotes has accepted input""" pass class InputRejected(Exception): """ Denotes that the user has rejected input""" pass netplan-1.0/python-cffi/000077500000000000000000000000001457004145200152435ustar00rootroot00000000000000netplan-1.0/python-cffi/meson.build000066400000000000000000000000221457004145200173770ustar00rootroot00000000000000subdir('netplan') netplan-1.0/python-cffi/netplan/000077500000000000000000000000001457004145200167045ustar00rootroot00000000000000netplan-1.0/python-cffi/netplan/__init__.py000066400000000000000000000055121457004145200210200ustar00rootroot00000000000000# Copyright (C) 2023 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from io import StringIO import json import os from typing import Union, List, IO from ._netplan_cffi import lib from .netdef import NetDefinition, NetDefinitionIterator from .parser import Parser from .state import State from ._utils import _checked_lib_call from ._utils import (NetplanException, NetplanBackendException, NetplanEmitterException, NetplanFileException, NetplanFormatException, NetplanParserException, NetplanValidationException) def _dump_yaml_subtree(prefix: List[str], input_file: IO, output_file: IO): if isinstance(input_file, StringIO): input_fd = os.memfd_create(name='netplan_temp_input_file') data = input_file.getvalue() os.write(input_fd, data.encode('utf-8')) os.lseek(input_fd, 0, os.SEEK_SET) else: input_fd = input_file.fileno() if isinstance(output_file, StringIO): output_fd = os.memfd_create(name='netplan_temp_output_file') else: output_fd = output_file.fileno() _checked_lib_call(lib.netplan_util_dump_yaml_subtree, '\t'.join(prefix).encode('utf-8'), input_fd, output_fd) if isinstance(input_file, StringIO): os.close(input_fd) if isinstance(output_file, StringIO): size = os.lseek(output_fd, 0, os.SEEK_CUR) os.lseek(output_fd, 0, os.SEEK_SET) data = os.read(output_fd, size) output_file.write(data.decode('utf-8')) os.close(output_fd) def _create_yaml_patch(patch_object_path: List[str], patch_payload: Union[str, dict], patch_output: IO): if isinstance(patch_payload, dict): patch_payload = json.dumps(patch_payload) _checked_lib_call(lib.netplan_util_create_yaml_patch, '\t'.join(patch_object_path).encode('utf-8'), patch_payload.encode('utf-8'), patch_output.fileno()) # Re-export submodules __all__ = [Parser, State, NetDefinition, NetDefinitionIterator, _dump_yaml_subtree, _create_yaml_patch, NetplanException, NetplanBackendException, NetplanEmitterException, NetplanFileException, NetplanFormatException, NetplanParserException, NetplanValidationException] netplan-1.0/python-cffi/netplan/_build_cffi.py000066400000000000000000000213541457004145200215100ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (C) 2023 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys from cffi import FFI ffibuilder = FFI() # cdef() expects a single string declaring the C types, functions and # globals needed to use the shared object. It must be in valid C syntax. ffibuilder.cdef(""" #define UINT_MAX ... typedef int gboolean; typedef unsigned int guint; typedef int gint; typedef struct GError NetplanError; typedef struct netplan_parser NetplanParser; typedef struct netplan_state NetplanState; typedef struct netplan_net_definition NetplanNetDefinition; typedef enum { ... } NetplanBackend; typedef enum { ... } NetplanDefType; // TODO: Introduce getters for .address/.lifetime/.label to avoid exposing the raw struct typedef struct { char* address; char* lifetime; char* label; } NetplanAddressOptions; struct address_iter { ...; }; struct nameserver_iter { ...; }; struct route_iter { ...; }; // TODO: Introduce getters for all these fields to avoid exposing the raw struct typedef struct { gint family; char* type; char* scope; guint table; char* from; char* to; char* via; gboolean onlink; guint metric; guint mtubytes; guint congestion_window; guint advertised_receive_window; } NetplanIPRoute; // Error handling uint64_t netplan_error_code(NetplanError* error); ssize_t netplan_error_message(NetplanError* error, char* buf, size_t buf_size); // Parser NetplanParser* netplan_parser_new(); void netplan_parser_clear(NetplanParser **npp); gboolean netplan_parser_load_yaml(NetplanParser* npp, const char* filename, NetplanError** error); gboolean netplan_parser_load_yaml_from_fd(NetplanParser* npp, int input_fd, NetplanError** error); gboolean netplan_parser_load_yaml_hierarchy(NetplanParser* npp, const char* rootdir, NetplanError** error); gboolean netplan_parser_load_keyfile(NetplanParser* npp, const char* filename, NetplanError** error); gboolean netplan_parser_load_nullable_fields(NetplanParser* npp, int input_fd, NetplanError** error); gboolean netplan_parser_load_nullable_overrides( NetplanParser* npp, int input_fd, const char* constraint, NetplanError** error); // State NetplanState* netplan_state_new(); void netplan_state_clear(NetplanState** np_state); NetplanBackend netplan_state_get_backend(const NetplanState* np_state); gboolean netplan_state_import_parser_results(NetplanState* np_state, NetplanParser* npp, NetplanError** error); gboolean netplan_state_update_yaml_hierarchy( const NetplanState* np_state, const char* default_filename, const char* rootdir, NetplanError** error); gboolean netplan_state_write_yaml_file( const NetplanState* np_state, const char* filename, const char* rootdir, NetplanError** error); gboolean netplan_state_dump_yaml(const NetplanState* np_state, int output_fd, NetplanError** error); NetplanNetDefinition* netplan_state_get_netdef(const NetplanState* np_state, const char* id); guint netplan_state_get_netdefs_size(const NetplanState* np_state); // NetDefinition ssize_t netplan_netdef_get_id(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buffer_size); NetplanDefType netplan_netdef_get_type(const NetplanNetDefinition* netdef); NetplanBackend netplan_netdef_get_backend(const NetplanNetDefinition* netdef); ssize_t netplan_netdef_get_filepath(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buffer_size); NetplanNetDefinition* netplan_netdef_get_bridge_link(const NetplanNetDefinition* netdef); NetplanNetDefinition* netplan_netdef_get_bond_link(const NetplanNetDefinition* netdef); NetplanNetDefinition* netplan_netdef_get_peer_link(const NetplanNetDefinition* netdef); NetplanNetDefinition* netplan_netdef_get_vlan_link(const NetplanNetDefinition* netdef); NetplanNetDefinition* netplan_netdef_get_sriov_link(const NetplanNetDefinition* netdef); NetplanNetDefinition* netplan_netdef_get_vrf_link(const NetplanNetDefinition* netdef); ssize_t netplan_netdef_get_set_name(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buffer_size); gboolean netplan_netdef_has_match(const NetplanNetDefinition* netdef); gboolean netplan_netdef_match_interface( const NetplanNetDefinition* netdef, const char* name, const char* mac, const char* driver_name); gboolean netplan_netdef_get_dhcp4(const NetplanNetDefinition* netdef); gboolean netplan_netdef_get_dhcp6(const NetplanNetDefinition* netdef); gboolean netplan_netdef_get_link_local_ipv4(const NetplanNetDefinition* netdef); gboolean netplan_netdef_get_link_local_ipv6(const NetplanNetDefinition* netdef); ssize_t netplan_netdef_get_macaddress(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buffer_size); // NetDefinition (internal) ssize_t _netplan_netdef_get_embedded_switch_mode(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size); gboolean _netplan_netdef_get_sriov_vlan_filter(const NetplanNetDefinition* netdef); guint _netplan_netdef_get_vlan_id(const NetplanNetDefinition* netdef); gboolean _netplan_netdef_get_critical(const NetplanNetDefinition* netdef); gboolean _netplan_netdef_get_delay_virtual_functions_rebind(const NetplanNetDefinition* netdef); gboolean _netplan_netdef_is_trivial_compound_itf(const NetplanNetDefinition* netdef); int _netplan_state_get_vf_count_for_def( const NetplanState* np_state, const NetplanNetDefinition* netdef, NetplanError** error); ssize_t _netplan_netdef_get_bond_mode(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size); // Iterators (internal) struct netdef_pertype_iter* _netplan_state_new_netdef_pertype_iter(NetplanState* np_state, const char* def_type); NetplanNetDefinition* _netplan_netdef_pertype_iter_next(struct netdef_pertype_iter* it); void _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it); struct address_iter* _netplan_netdef_new_address_iter(NetplanNetDefinition* netdef); NetplanAddressOptions* _netplan_address_iter_next(struct address_iter* it); void _netplan_address_iter_free(struct address_iter* it); struct nameserver_iter* _netplan_netdef_new_nameserver_iter(NetplanNetDefinition* netdef); char* _netplan_nameserver_iter_next(struct nameserver_iter* it); void _netplan_nameserver_iter_free(struct nameserver_iter* it); struct nameserver_iter* _netplan_netdef_new_search_domain_iter(NetplanNetDefinition* netdef); char* _netplan_search_domain_iter_next(struct nameserver_iter* it); void _netplan_search_domain_iter_free(struct nameserver_iter* it); struct route_iter* _netplan_netdef_new_route_iter(NetplanNetDefinition* netdef); NetplanIPRoute* _netplan_route_iter_next(struct route_iter* it); void _netplan_route_iter_free(struct route_iter* it); // Utils gboolean netplan_util_dump_yaml_subtree(const char* prefix, int input_fd, int output_fd, NetplanError** error); gboolean netplan_util_create_yaml_patch(const char* conf_obj_path, const char* obj_payload, int out_fd, NetplanError** error); // Names (internal) const char* netplan_backend_name(NetplanBackend val); const char* netplan_def_type_name(NetplanDefType val); """) cffi_inc = os.getenv('CFFI_INC', sys.argv[1]) cffi_lib = os.getenv('CFFI_LIB', sys.argv[2]) # set_source() gives the name of the python extension module to # produce, and some C source code as a string. This C code needs # to make the declarated functions, types and globals available, # so it is often just the "#include". ffibuilder.set_source_pkgconfig( "_netplan_cffi", ['glib-2.0'], """ #include // C API of libnetplan.so #include "netplan.h" #include "parse.h" #include "parse-nm.h" #include "util.h" // internal headers (private API) #include "util-internal.h" #include "names.h" """, include_dirs=[cffi_inc], library_dirs=[cffi_lib], libraries=['glib-2.0']) # library name, for the linker if __name__ == "__main__": ffibuilder.distutils_extension('.') netplan-1.0/python-cffi/netplan/_utils.py000066400000000000000000000166331457004145200205660ustar00rootroot00000000000000# Copyright (C) 2023 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from collections import defaultdict from enum import IntEnum import re from ._netplan_cffi import ffi, lib # Errors and error domains # NOTE: if new errors or domains are added, # include/types.h must be updated with the new entries class NETPLAN_ERROR_DOMAINS(IntEnum): NETPLAN_PARSER_ERROR = 1 NETPLAN_VALIDATION_ERROR = 2 NETPLAN_FILE_ERROR = 3 NETPLAN_BACKEND_ERROR = 4 NETPLAN_EMITTER_ERROR = 5 NETPLAN_FORMAT_ERROR = 6 class NETPLAN_PARSER_ERRORS(IntEnum): NETPLAN_ERROR_INVALID_YAML = 0 NETPLAN_ERROR_INVALID_CONFIG = 1 class NETPLAN_VALIDATION_ERRORS(IntEnum): NETPLAN_ERROR_CONFIG_GENERIC = 0 NETPLAN_ERROR_CONFIG_VALIDATION = 1 class NETPLAN_BACKEND_ERRORS(IntEnum): NETPLAN_ERROR_UNSUPPORTED = 0 NETPLAN_ERROR_VALIDATION = 1 class NETPLAN_EMITTER_ERRORS(IntEnum): NETPLAN_ERROR_YAML_EMITTER = 0 class NETPLAN_FORMAT_ERRORS(IntEnum): NETPLAN_ERROR_FORMAT_INVALID_YAML = 0 class NetplanException(Exception): def __init__(self, message=None, domain=None, error=None): self.domain = domain self.error = error self.message = message def __str__(self): return self.message class NetplanFileException(NetplanException): @property def errno(self): return self.error class NetplanValidationException(NetplanException): ''' Netplan Validation errors are expected to contain the YAML file name from where the error was found. A validation error might happen after the parsing stage. libnetplan walks through its internal representation of the network configuration and checks if all the requirements are met. For example, if it finds that the key "set-name" is used by an interface, it will check if "match" is present. As "set-name" requires "match" to work, it will emit a validation error if it's not found. ''' SCHEMA_VALIDATION_ERROR_MSG_REGEX = ( r'(?P.*\.yaml): (?P.*)' ) def __init__(self, message=None, domain=None, error=None): super().__init__(message, domain, error) schema_error = re.match(self.SCHEMA_VALIDATION_ERROR_MSG_REGEX, message) if not schema_error: # This shouldn't happen raise ValueError(f'The validation error message does not have the expected format: {message}') self.filename = schema_error["file_path"] self.message = schema_error["message"] class NetplanParserException(NetplanException): ''' Netplan Parser errors are expected to contain the YAML file name and line and column numbers from where the error was found. A parser error might happen during the parsing stage. Parsing errors might be due to invalid YAML files or invalid Netplan grammar. libnetplan will check for this kind of issues while it's walking through the YAML files, so it has access to the location where the error was found. ''' SCHEMA_PARSER_ERROR_MSG_REGEX = ( r'(?P.*):(?P\d+):(?P\d+): (?P(\s|.)*)' ) def __init__(self, message=None, domain=None, error=None): super().__init__(message, domain, error) # Parser errors from libnetplan have the form: # # filename.yaml:4:14: Error in network definition: invalid boolean value 'falsea' # schema_error = re.match(self.SCHEMA_PARSER_ERROR_MSG_REGEX, message) if not schema_error: # This shouldn't happen raise ValueError(f'The parser error message does not have the expected format: {message}') self.filename = schema_error["file_path"] self.line = schema_error["error_line"] self.column = schema_error["error_col"] self.message = schema_error["message"] class NetplanBackendException(NetplanException): pass class NetplanEmitterException(NetplanException): pass class NetplanFormatException(NetplanException): pass # Used in case the "domain" received from libnetplan doesn't exist NETPLAN_EXCEPTIONS_FALLBACK = defaultdict(lambda: NetplanException) # If a domain that doesn't exist is queried, it will fallback to NETPLAN_EXCEPTIONS_FALLBACK # which will return NetplanException for any key accessed. NETPLAN_EXCEPTIONS = defaultdict(lambda: NETPLAN_EXCEPTIONS_FALLBACK, { NETPLAN_ERROR_DOMAINS.NETPLAN_PARSER_ERROR: { NETPLAN_PARSER_ERRORS.NETPLAN_ERROR_INVALID_YAML: NetplanParserException, NETPLAN_PARSER_ERRORS.NETPLAN_ERROR_INVALID_CONFIG: NetplanParserException, }, NETPLAN_ERROR_DOMAINS.NETPLAN_VALIDATION_ERROR: { NETPLAN_VALIDATION_ERRORS.NETPLAN_ERROR_CONFIG_GENERIC: NetplanException, NETPLAN_VALIDATION_ERRORS.NETPLAN_ERROR_CONFIG_VALIDATION: NetplanValidationException, }, # FILE_ERRORS are "errno" values and they all throw the same exception NETPLAN_ERROR_DOMAINS.NETPLAN_FILE_ERROR: defaultdict(lambda: NetplanFileException), NETPLAN_ERROR_DOMAINS.NETPLAN_BACKEND_ERROR: { NETPLAN_BACKEND_ERRORS.NETPLAN_ERROR_UNSUPPORTED: NetplanBackendException, NETPLAN_BACKEND_ERRORS.NETPLAN_ERROR_VALIDATION: NetplanBackendException, }, NETPLAN_ERROR_DOMAINS.NETPLAN_EMITTER_ERROR: { NETPLAN_EMITTER_ERRORS.NETPLAN_ERROR_YAML_EMITTER: NetplanEmitterException, }, NETPLAN_ERROR_DOMAINS.NETPLAN_FORMAT_ERROR: { NETPLAN_FORMAT_ERRORS.NETPLAN_ERROR_FORMAT_INVALID_YAML: NetplanFormatException, } }) def _checked_lib_call(fn, *args): ref = ffi.new('NetplanError **') ret = bool(fn(*args, ref)) if not ret: err = ref[0] if err == ffi.NULL: # pragma: nocover (should never happen) raise NetplanException("Unknown error", 0, 0) domain_code = lib.netplan_error_code(err) error_domain = domain_code >> 32 # upper 32 bits error_code = int(ffi.cast('uint32_t', domain_code)) # lower 32 bits error_message = _string_realloc_call_no_error(lambda b: lib.netplan_error_message(err, b, len(b))) exception = NETPLAN_EXCEPTIONS[error_domain][error_code] raise exception(error_message, error_domain, error_code) return ret def _string_realloc_call_no_error(function: callable): size = 16 while size < 1048576: # 1MB buf = ffi.new('char[]', size) code = function(buf) if code == -2: size = size * 2 continue if code < 0: # pragma: nocover raise NetplanException("Unknown error: %d" % code) elif code == 0: return None # pragma: nocover as it's hard to trigger for now else: return ffi.string(buf).decode('utf-8') raise NetplanException('Halting due to string buffer size > 1M') # pragma: nocover netplan-1.0/python-cffi/netplan/meson.build000066400000000000000000000023511457004145200210470ustar00rootroot00000000000000pymod = import('python') python = pymod.find_installation( 'python3', modules: ['cffi'] ) python_dep = python.dependency(required: true) cffi_srcs = configure_file( command: [ python, files('_build_cffi.py'), join_paths(meson.project_source_root(), 'include'), join_paths(meson.current_build_dir(), 'src'), ], output: '_netplan_cffi.c', ) # Generation of the Python binary extension through meson. cffi_pyext = python.extension_module( '_netplan_cffi', cffi_srcs, dependencies: [python_dep, glib, uuid], include_directories: [inc, inc_internal], link_with: [libnetplan], subdir: 'netplan', install: true, ) bindings_sources = [ '__init__.py', 'netdef.py', 'parser.py', 'state.py', '_utils.py'] # Copy module sources into build-dir, # so they can be importet together with the binary extension foreach src : bindings_sources custom_target( input: src, output: src, command: ['cp', '@INPUT@', join_paths(meson.current_build_dir(), '@PLAINNAME@')], build_always_stale: true, build_by_default: true, depends: cffi_pyext) endforeach bindings = python.install_sources( [bindings_sources], subdir: 'netplan') netplan-1.0/python-cffi/netplan/netdef.py000066400000000000000000000307361457004145200205340ustar00rootroot00000000000000# Copyright (C) 2023 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from dataclasses import dataclass from ._netplan_cffi import ffi, lib from ._utils import _string_realloc_call_no_error, NetplanException class NetDefinition(): def __init__(self, np_state, ptr): self._ptr = ptr # We hold on to this to avoid the underlying pointer being invalidated by # the GC invoking netplan_state_free self._parent = np_state def __eq__(self, other: 'NetDefinition') -> bool: if not hasattr(other, '_ptr'): return False return self._ptr == other._ptr def _match_interface(self, iface_name: str = None, iface_driver: str = None, iface_mac: str = None) -> bool: return bool(lib.netplan_netdef_match_interface( self._ptr, iface_name.encode('utf-8') if iface_name else ffi.NULL, iface_mac.encode('utf-8') if iface_mac else ffi.NULL, iface_driver.encode('utf-8') if iface_driver else ffi.NULL)) @property def addresses(self) -> '_NetdefAddressIterator': return _NetdefAddressIterator(self._ptr) @property def dhcp4(self) -> bool: return bool(lib.netplan_netdef_get_dhcp4(self._ptr)) @property def dhcp6(self) -> bool: return bool(lib.netplan_netdef_get_dhcp6(self._ptr)) @property def link_local(self) -> list: linklocal = [] if bool(lib.netplan_netdef_get_link_local_ipv4(self._ptr)): linklocal.append('ipv4') if bool(lib.netplan_netdef_get_link_local_ipv6(self._ptr)): linklocal.append('ipv6') return linklocal @property def nameserver_addresses(self) -> '_NetdefNameserverIterator': return _NetdefNameserverIterator(self._ptr) @property def nameserver_search(self) -> '_NetdefSearchDomainIterator': return _NetdefSearchDomainIterator(self._ptr) @property def routes(self) -> '_NetdefRouteIterator': return _NetdefRouteIterator(self._ptr) @property def macaddress(self) -> str: return _string_realloc_call_no_error(lambda b: lib.netplan_netdef_get_macaddress(self._ptr, b, len(b))) @property def _has_match(self) -> bool: return bool(lib.netplan_netdef_has_match(self._ptr)) @property def set_name(self) -> str: return _string_realloc_call_no_error(lambda b: lib.netplan_netdef_get_set_name(self._ptr, b, len(b))) @property def critical(self) -> bool: return bool(lib._netplan_netdef_get_critical(self._ptr)) @property def links(self) -> dict: d = dict() if sriov_link := lib.netplan_netdef_get_sriov_link(self._ptr): d['sriov'] = NetDefinition(self._parent, sriov_link) if vlan_link := lib.netplan_netdef_get_vlan_link(self._ptr): d['vlan'] = NetDefinition(self._parent, vlan_link) if bridge_link := lib.netplan_netdef_get_bridge_link(self._ptr): d['bridge'] = NetDefinition(self._parent, bridge_link) if bond_link := lib.netplan_netdef_get_bond_link(self._ptr): d['bond'] = NetDefinition(self._parent, bond_link) if vrf_link := lib.netplan_netdef_get_vrf_link(self._ptr): d['vrf'] = NetDefinition(self._parent, vrf_link) # TODO: ovs vs veth? Should we use the same field? if peer_link := lib.netplan_netdef_get_peer_link(self._ptr): d['peer'] = NetDefinition(self._parent, peer_link) return d @property def _vlan_id(self) -> int: vlan_id = lib._netplan_netdef_get_vlan_id(self._ptr) if vlan_id == lib.UINT_MAX: return None return vlan_id @property def _has_sriov_vlan_filter(self) -> bool: return bool(lib._netplan_netdef_get_sriov_vlan_filter(self._ptr)) @property def backend(self) -> str: return ffi.string(lib.netplan_backend_name(lib.netplan_netdef_get_backend(self._ptr))).decode('utf-8') @property def type(self) -> str: return ffi.string(lib.netplan_def_type_name(lib.netplan_netdef_get_type(self._ptr))).decode('utf-8') @property def id(self) -> str: return _string_realloc_call_no_error(lambda b: lib.netplan_netdef_get_id(self._ptr, b, len(b))) @property def filepath(self) -> str: return _string_realloc_call_no_error(lambda b: lib.netplan_netdef_get_filepath(self._ptr, b, len(b))) @property def _embedded_switch_mode(self) -> str: return _string_realloc_call_no_error(lambda b: lib._netplan_netdef_get_embedded_switch_mode(self._ptr, b, len(b))) @property def _delay_virtual_functions_rebind(self) -> bool: return bool(lib._netplan_netdef_get_delay_virtual_functions_rebind(self._ptr)) @property def _vf_count(self) -> int: ref = ffi.new('NetplanError **') count = lib._netplan_state_get_vf_count_for_def(self._parent._ptr, self._ptr, ref) if count < 0: err = ref[0] msg = _string_realloc_call_no_error(lambda b: lib.netplan_error_message(err, b, len(b))) raise NetplanException(msg) return count @property def _bond_mode(self) -> str: return _string_realloc_call_no_error(lambda b: lib._netplan_netdef_get_bond_mode(self._ptr, b, len(b))) @property def _is_trivial_compound_itf(self) -> bool: ''' Returns True if the interface is a compound interface (bond or bridge), and its configuration is trivial, without any variation from the defaults. ''' return bool(lib._netplan_netdef_is_trivial_compound_itf(self._ptr)) class NetDefinitionIterator(): def __init__(self, np_state, dev_type: str = None): # To keep things valid, keep a reference to the parent state self.np_state = np_state np_type = dev_type.encode('utf-8') if dev_type else ffi.NULL self.iterator = lib._netplan_state_new_netdef_pertype_iter(np_state._ptr, np_type) def __del__(self): lib._netplan_netdef_pertype_iter_free(self.iterator) def __iter__(self): return self def __next__(self): next_value = lib._netplan_netdef_pertype_iter_next(self.iterator) if not next_value: raise StopIteration return NetDefinition(self.np_state, next_value) class NetplanAddress: def __init__(self, address: str, lifetime: str, label: str): self.address = address self.lifetime = lifetime self.label = label def __str__(self) -> str: return self.address class _NetdefAddressIterator: def __init__(self, netdef: NetDefinition): self.netdef = netdef self.iterator = lib._netplan_netdef_new_address_iter(netdef) def __del__(self): lib._netplan_address_iter_free(self.iterator) def __iter__(self): return self def __next__(self): next_value = lib._netplan_address_iter_next(self.iterator) if not next_value: raise StopIteration content = next_value # XXX: Introduce getters for .address/.lifetime/.label, to avoid # exposing the 'address_iter' struct in _netplan_cffi.so address = ffi.string(content.address).decode('utf-8') if content.address else None lifetime = ffi.string(content.lifetime).decode('utf-8') if content.lifetime else None label = ffi.string(content.label).decode('utf-8') if content.label else None return NetplanAddress(address, lifetime, label) class _NetdefNameserverIterator: def __init__(self, netdef: NetDefinition): self.netdef = netdef self.iterator = lib._netplan_netdef_new_nameserver_iter(netdef) def __del__(self): lib._netplan_nameserver_iter_free(self.iterator) def __iter__(self): return self def __next__(self): next_value = lib._netplan_nameserver_iter_next(self.iterator) if not next_value: raise StopIteration return ffi.string(next_value).decode('utf-8') class _NetdefSearchDomainIterator: def __init__(self, netdef): self.netdef = netdef self.iterator = lib._netplan_netdef_new_search_domain_iter(netdef) def __del__(self): lib._netplan_search_domain_iter_free(self.iterator) def __iter__(self): return self def __next__(self): next_value = lib._netplan_search_domain_iter_next(self.iterator) if not next_value: raise StopIteration return ffi.string(next_value).decode('utf-8') @dataclass class NetplanRoute: _METRIC_UNSPEC_ = lib.UINT_MAX _TABLE_UNSPEC_ = 0 to: str = None via: str = None from_addr: str = None type: str = 'unicast' scope: str = 'global' protocol: str = None table: int = _TABLE_UNSPEC_ family: int = -1 metric: int = _METRIC_UNSPEC_ mtubytes: int = 0 congestion_window: int = 0 advertised_receive_window: int = 0 onlink: bool = False def __str__(self): route = "" if self.to: route = route + self.to if self.via: route = route + f' via {self.via}' if self.type: route = route + f' type {self.type}' if self.scope: route = route + f' scope {self.scope}' if self.from_addr: route = route + f' src {self.from_addr}' if self.metric < self._METRIC_UNSPEC_: route = route + f' metric {self.metric}' if self.table > self._TABLE_UNSPEC_: route = route + f' table {self.table}' return route.strip() def to_dict(self): route = {} if self.family >= 0: route['family'] = self.family if self.to: route['to'] = self.to if self.via: route['via'] = self.via if self.from_addr: route['from'] = self.from_addr if self.metric < self._METRIC_UNSPEC_: route['metric'] = self.metric if self.table > self._TABLE_UNSPEC_: route['table'] = self.table route['type'] = self.type return route def __hash__(self): return hash( (self.to, self.via, self.from_addr, self.table, self.family, self.metric, self.type, self.scope)) def __eq__(self, route): return ( self.to == route.to and self.via == route.via and self.from_addr == route.from_addr and self.table == route.table and self.family == route.family and self.metric == route.metric and self.type == route.type and self.scope == route.scope ) class _NetdefRouteIterator: def __init__(self, netdef): self.netdef = netdef self.iterator = lib._netplan_netdef_new_route_iter(netdef) def __del__(self): lib._netplan_route_iter_free(self.iterator) def __iter__(self): return self def __next__(self): next_value = lib._netplan_route_iter_next(self.iterator) if not next_value: raise StopIteration # The field 'from' happens to be a reserved keyword in Python from_addr = getattr(next_value, 'from') route = { 'to': ffi.string(next_value.to).decode('utf-8') if next_value.to else None, 'via': ffi.string(next_value.via).decode('utf-8') if next_value.via else None, 'from_addr': ffi.string(from_addr).decode('utf-8') if from_addr else None, 'type': ffi.string(next_value.type).decode('utf-8') if next_value.type else None, 'scope': ffi.string(next_value.scope).decode('utf-8') if next_value.scope else None, 'protocol': None, 'table': next_value.table, 'family': next_value.family, 'metric': next_value.metric, 'mtubytes': next_value.mtubytes, 'congestion_window': next_value.congestion_window, 'advertised_receive_window': next_value.advertised_receive_window, 'onlink': next_value.onlink } return NetplanRoute(**route) netplan-1.0/python-cffi/netplan/parser.py000066400000000000000000000040741457004145200205570ustar00rootroot00000000000000# Copyright (C) 2023 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from typing import Union, IO from ._netplan_cffi import ffi, lib from ._utils import _checked_lib_call class Parser(): def __init__(self): self._ptr = lib.netplan_parser_new() def __del__(self): ref = ffi.new('NetplanParser **', self._ptr) lib.netplan_parser_clear(ref) def load_yaml(self, input_file: Union[str, IO]): if isinstance(input_file, str): return _checked_lib_call(lib.netplan_parser_load_yaml, self._ptr, input_file.encode('utf-8')) else: return _checked_lib_call(lib.netplan_parser_load_yaml_from_fd, self._ptr, input_file.fileno()) def load_yaml_hierarchy(self, rootdir: str = None): root = rootdir.encode('utf-8') if rootdir else ffi.NULL return _checked_lib_call(lib.netplan_parser_load_yaml_hierarchy, self._ptr, root) def load_keyfile(self, input_file: str): # TODO: load from File/fd (i.e. input_file: Union[str, IO]) return _checked_lib_call(lib.netplan_parser_load_keyfile, self._ptr, input_file.encode('utf-8')) def load_nullable_fields(self, input_file: IO): return _checked_lib_call(lib.netplan_parser_load_nullable_fields, self._ptr, input_file.fileno()) def _load_nullable_overrides(self, input_file: IO, constraint: str): return _checked_lib_call(lib.netplan_parser_load_nullable_overrides, self._ptr, input_file.fileno(), constraint.encode('utf-8')) netplan-1.0/python-cffi/netplan/state.py000066400000000000000000000117111457004145200203770ustar00rootroot00000000000000# Copyright (C) 2023 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from enum import IntEnum from io import StringIO import os from typing import IO from ._netplan_cffi import ffi, lib from .netdef import NetDefinition, NetDefinitionIterator from .parser import Parser from ._utils import _checked_lib_call # class NETPLAN_STORAGE(IntEnum): # ETC = 0 # RUN = 1 # LIB = 2 class State(): def __init__(self): self._ptr = lib.netplan_state_new() def __del__(self): ref = ffi.new('NetplanState **', self._ptr) lib.netplan_state_clear(ref) def __getitem__(self, netdef_id: str): ptr = lib.netplan_state_get_netdef(self._ptr, netdef_id.encode('utf-8')) if not ptr: raise IndexError() return NetDefinition(self, ptr) def __len__(self): return lib.netplan_state_get_netdefs_size(self._ptr) def import_parser_results(self, parser: Parser): _checked_lib_call(lib.netplan_state_import_parser_results, self._ptr, parser._ptr) # def write_yaml(filter: str, default_filename: str = None, # storage: NETPLAN_STORAGE = NETPLAN_STORAGE.ETC, rootdir str = None): # # TODO: https://bugs.launchpad.net/netplan/+bug/2003727 # raise NotImplementedError def _write_yaml_file(self, filename: str = None, rootdir: str = None): name = filename.encode('utf-8') if filename else ffi.NULL root = rootdir.encode('utf-8') if rootdir else ffi.NULL _checked_lib_call(lib.netplan_state_write_yaml_file, self._ptr, name, root) def _update_yaml_hierarchy(self, default_filename: str, rootdir: str = None): name = default_filename.encode('utf-8') root = rootdir.encode('utf-8') if rootdir else ffi.NULL _checked_lib_call(lib.netplan_state_update_yaml_hierarchy, self._ptr, name, root) def _dump_yaml(self, output_file: IO): if isinstance(output_file, StringIO): fd = os.memfd_create(name='netplan_temp_file') _checked_lib_call(lib.netplan_state_dump_yaml, self._ptr, fd) size = os.lseek(fd, 0, os.SEEK_CUR) os.lseek(fd, 0, os.SEEK_SET) data = os.read(fd, size) os.close(fd) output_file.write(data.decode('utf-8')) else: fd = output_file.fileno() _checked_lib_call(lib.netplan_state_dump_yaml, self._ptr, fd) @property def backend(self) -> str: return ffi.string(lib.netplan_backend_name(lib.netplan_state_get_backend(self._ptr))).decode('utf-8') @property def netdefs(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, None)) @property def ethernets(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "ethernets")) @property def modems(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "modems")) @property def wifis(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "wifis")) @property def vlans(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "vlans")) @property def bridges(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "bridges")) @property def bonds(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "bonds")) @property def dummy_devices(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "dummy-devices")) @property def tunnels(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "tunnels")) @property def virtual_ethernets(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "virtual-ethernets")) @property def vrfs(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "vrfs")) @property def ovs_ports(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "_ovs-ports")) @property def nm_devices(self) -> NetDefinitionIterator: return dict((nd.id, nd) for nd in NetDefinitionIterator(self, "nm-devices")) netplan-1.0/rpm/000077500000000000000000000000001457004145200136135ustar00rootroot00000000000000netplan-1.0/rpm/netplan.spec000066400000000000000000000217741457004145200161430ustar00rootroot00000000000000# Ubuntu calls their own software netplan.io in the archive due to name conflicts %global ubuntu_name netplan.io # If this isn't defined, define it %{?!_systemdgeneratordir:%global _systemdgeneratordir /usr/lib/systemd/system-generators} # Netplan library soversion major %global libsomajor 1 # networkd is not available everywhere %if 0%{?rhel} %bcond_with networkd_support %else %bcond_without networkd_support %endif Name: netplan Version: 0.106 Release: 0%{?dist} Summary: Network configuration tool using YAML Group: System Environment/Base License: GPL-3.0-only URL: http://netplan.io/ Source0: https://github.com/canonical/%{name}/archive/%{version}/%{name}-%{version}.tar.gz BuildRequires: gcc BuildRequires: meson >= 0.61 BuildRequires: pkgconfig(bash-completion) BuildRequires: pkgconfig(glib-2.0) BuildRequires: pkgconfig(gio-2.0) BuildRequires: pkgconfig(libsystemd) BuildRequires: pkgconfig(systemd) BuildRequires: pkgconfig(yaml-0.1) BuildRequires: pkgconfig(uuid) BuildRequires: python3-devel BuildRequires: python3-cffi BuildRequires: systemd-rpm-macros BuildRequires: %{_bindir}/pandoc BuildRequires: %{_bindir}/find # For tests BuildRequires: %{_sbindir}/ip BuildRequires: pkgconfig(cmocka) BuildRequires: python3dist(coverage) BuildRequires: dbus-x11 BuildRequires: python3dist(netifaces) BuildRequires: python3dist(pycodestyle) BuildRequires: python3dist(pyflakes) BuildRequires: python3dist(pytest) BuildRequires: python3dist(pytest-cov) BuildRequires: python3dist(pyyaml) BuildRequires: python3dist(rich) BuildRequires: %{_bindir}/ovs-vsctl # /usr/sbin/netplan is a Python 3 script that requires Python modules Requires: python3dist(netifaces) Requires: python3dist(pyyaml) Requires: python3dist(rich) # 'ip' command is used in netplan apply subcommand Requires: %{_sbindir}/ip # netplan ships dbus files Requires: dbus-common # Netplan requires a backend for configuration Requires: %{name}-default-backend # Prefer NetworkManager Suggests: %{name}-default-backend-NetworkManager # Netplan requires its core libraries Requires: %{name}-libs%{?_isa} = %{version}-%{release} # Provide the package name that Ubuntu uses for it too... Provides: %{ubuntu_name} = %{version}-%{release} Provides: %{ubuntu_name}%{?_isa} = %{version}-%{release} %description netplan reads network configuration from /etc/netplan/*.yaml which are written by administrators, installers, cloud image instantiations, or other OS deployments. During early boot, it generates backend specific configuration files in /run to hand off control of devices to a particular networking daemon. Currently supported backends are NetworkManager and systemd-networkd. %files %license COPYING %doc %{_docdir}/%{name}/ %{_sbindir}/%{name} %{_datadir}/%{name}/ %{_datadir}/dbus-1/system-services/io.netplan.Netplan.service %{_datadir}/dbus-1/system.d/io.netplan.Netplan.conf %{_systemdgeneratordir}/%{name} %{_mandir}/man5/%{name}.5* %{_mandir}/man8/%{name}*.8* %dir %{_sysconfdir}/%{name} %dir %{_prefix}/lib/%{name} %{_libexecdir}/%{name}/ %{_datadir}/bash-completion/completions/%{name} %{python3_sitelib}/%{name}/ %{python3_sitearch}/%{name}/ # ------------------------------------------------------------------------------------------------ %package libs Summary: Network configuration tool using YAML (core library) Group: System Environment/Libraries %description libs netplan reads network configuration from /etc/netplan/*.yaml which are written by administrators, installers, cloud image instantiations, or other OS deployments. During early boot, it generates backend specific configuration files in /run to hand off control of devices to a particular networking daemon. This package provides Netplan's core libraries. %files libs %license COPYING %{_libdir}/libnetplan.so.%{libsomajor}{,.*} # ------------------------------------------------------------------------------------------------ %package devel Summary: Network configuration tool using YAML (development files) Group: Development/Libraries Requires: %{name}-libs%{?_isa} = %{version}-%{release} %description devel netplan reads network configuration from /etc/netplan/*.yaml which are written by administrators, installers, cloud image instantiations, or other OS deployments. During early boot, it generates backend specific configuration files in /run to hand off control of devices to a particular networking daemon. This package provides development headers and libraries for building applications using Netplan. %files devel %{_includedir}/%{name}/ %{_libdir}/libnetplan.so %{_libdir}/pkgconfig/%{name}.pc # ------------------------------------------------------------------------------------------------ %package default-backend-NetworkManager Summary: Network configuration tool using YAML (NetworkManager backend) Group: System Environment/Base Requires: %{name} = %{version}-%{release} # Netplan requires NetworkManager for configuration Requires: NetworkManager # Disable NetworkManager's autoconfiguration Requires: NetworkManager-config-server # Generally, if linux-firmware-whence is installed, we want Wi-Fi capabilities Recommends: (NetworkManager-wifi if linux-firmware-whence) Suggests: NetworkManager-wifi # One and only one default backend permitted Conflicts: %{name}-default-backend Provides: %{name}-default-backend BuildArch: noarch %description default-backend-NetworkManager netplan reads network configuration from /etc/netplan/*.yaml which are written by administrators, installers, cloud image instantiations, or other OS deployments. During early boot, it generates backend specific configuration files in /run to hand off control of devices to a particular networking daemon. This package configures Netplan to use NetworkManager as its backend. %files default-backend-NetworkManager %{_prefix}/lib/%{name}/00-netplan-default-renderer-nm.yaml # ------------------------------------------------------------------------------------------------ %if %{with networkd_support} %package default-backend-networkd Summary: Network configuration tool using YAML (systemd-networkd backend) Group: System Environment/Base Requires: %{name} = %{version}-%{release} # Netplan requires systemd-networkd for configuration Requires: systemd-networkd # Generally, if linux-firmware-whence is installed, we want Wi-Fi capabilities Recommends: (wpa_supplicant if linux-firmware-whence) Suggests: wpa_supplicant # One and only one default backend permitted Conflicts: %{name}-default-backend Provides: %{name}-default-backend BuildArch: noarch %description default-backend-networkd netplan reads network configuration from /etc/netplan/*.yaml which are written by administrators, installers, cloud image instantiations, or other OS deployments. During early boot, it generates backend specific configuration files in /run to hand off control of devices to a particular networking daemon. This package configures Netplan to use systemd-networkd as its backend. %files default-backend-networkd %{_prefix}/lib/%{name}/00-netplan-default-renderer-networkd.yaml %endif # ------------------------------------------------------------------------------------------------ %prep %autosetup -p1 # Drop -Werror to avoid the following error: # /usr/include/glib-2.0/glib/glib-autocleanups.h:28:3: error: 'ip_str' may be used uninitialized in this function [-Werror=maybe-uninitialized] sed -e "s/werror=true/werror=false/g" -i meson.build %build %meson %meson_build %install %meson_install # Remove superfluous __pycache__ rm -rf %{buildroot}/usr/lib/python3.11/site-packages/netplan/__pycache__ # Pre-create the config directories mkdir -p %{buildroot}%{_sysconfdir}/%{name} mkdir -p %{buildroot}%{_prefix}/lib/%{name} # Generate Netplan default renderer configuration cat > %{buildroot}%{_prefix}/lib/%{name}/00-netplan-default-renderer-nm.yaml < %{buildroot}%{_prefix}/lib/%{name}/00-netplan-default-renderer-networkd.yaml < - 0.106-0 - Update to 0.106 - Resync with Fedora spec - Drop EL7 and EL8 support * Thu Aug 18 2022 Lukas Märdian - 0.105-0 - Update to 0.105 * Sun Feb 20 2022 Neal Gompa - 0.104-0 - Update to 0.104 - Resync with Fedora spec * Fri Dec 14 2018 Mathieu Trudel-Lapierre - 0.95 - Update to 0.95 * Sat Oct 13 2018 Neal Gompa - 0.40.3-0 - Rebase to 0.40.3 * Tue Mar 13 2018 Neal Gompa - 0.34-0.1 - Update to 0.34 * Wed Mar 7 2018 Neal Gompa - 0.33-0.1 - Rebase to 0.33 * Sat Nov 4 2017 Neal Gompa - 0.30-1 - Rebase to 0.30 * Sun Jul 2 2017 Neal Gompa - 0.23~17.04.1-1 - Initial packaging netplan-1.0/sitecustomize.py000066400000000000000000000000531457004145200162740ustar00rootroot00000000000000import coverage coverage.process_startup() netplan-1.0/snap/000077500000000000000000000000001457004145200137565ustar00rootroot00000000000000netplan-1.0/snap/snapcraft.yaml000066400000000000000000000021061457004145200166220ustar00rootroot00000000000000name: netplan version: git summary: Backend-agnostic network configuration in YAML description: | Netplan is a utility for easily configuring networking on a linux system. You simply create a YAML description of the required network interfaces and what each should be configured to do. From this description Netplan will generate all the necessary configuration for your chosen renderer tool. grade: devel confinement: classic apps: netplan: command: usr/sbin/netplan environment: PYTHONPATH: $PYTHONPATH:$SNAP/usr/lib/python3/dist-packages parts: netplan: source: https://github.com/canonical/netplan.git plugin: make build-packages: - bash-completion - libglib2.0-dev - libyaml-dev - uuid-dev - pandoc - pkg-config - python3 - python3-coverage - python3-yaml - python3-netifaces - python3-pytest - python3-pytest-cov - pyflakes3 - pep8 - systemd stage-packages: - iproute2 - python3 - python3-netifaces - python3-yaml - systemd netplan-1.0/spread.yaml000066400000000000000000000034311457004145200151600ustar00rootroot00000000000000project: netplan backends: lxd: systems: [ubuntu-22.04] qemu: systems: - ubuntu-22.04-64: username: ubuntu password: ubuntu suites: tests/spread/: summary: integration tests path: /home/tests prepare: | # FIXME: remove after legacy symlink in 107 gets dropped (see meson.build) # This is needed as the git netplan puts a symlink to /lib/netplan/generate # but the package has the real generate there rm -f /lib/netplan/generate # FIXME: having the debian packging available would allow "apt # build-dep -y ./" would make this easier :) apt update -qq apt install -y ubuntu-dev-tools devscripts equivs pull-lp-source netplan.io sed -i 's| openvswitch-switch|# DELETED: openvswitch-switch|' netplan.io-*/debian/control sed -i 's| systemd-dev|# DELETED: systemd-dev|' netplan.io-*/debian/control mk-build-deps -i -r -B -s sudo -t "apt-get -y -o Debug::pkgProblemResolver=yes --no-install-recommends" netplan.io-*/debian/control # install, a bit ugly but this is a container (did I mention the packaging?) meson setup build --prefix=/usr meson compile -C build #meson test -C build --verbose, cannot run OVS test in container rm -rf /usr/share/netplan/netplan # clear (old) system installation meson install -C build --destdir=/ # set some defaults cat > /etc/netplan/0-snapd-defaults.yaml <<'EOF' network: version: 2 bridges: br54: dhcp4: true EOF chmod 0600 /etc/netplan/0-snapd-defaults.yaml echo "Precondition check, the basics work" netplan get bridges.br54.dhcp4 | MATCH true # keep original config around tar cvf "$SPREAD_PATH"/etc-netplan.tar.gz /etc/netplan/ restore-each: | # restore original netplan dir rm -rf /etc/netplan/* (cd / && tar xvf "$SPREAD_PATH"/etc-netplan.tar.gz) netplan-1.0/src/000077500000000000000000000000001457004145200136045ustar00rootroot00000000000000netplan-1.0/src/abi.h000066400000000000000000000265771457004145200145310ustar00rootroot00000000000000/* * Copyright (C) 2022 Canonical, Ltd. * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include typedef int NetplanFlags; /* Those types are part of our ABI as they have been exposed in older versions */ typedef enum { NETPLAN_OPTIONAL_IPV4_LL = 1<<0, NETPLAN_OPTIONAL_IPV6_RA = 1<<1, NETPLAN_OPTIONAL_DHCP4 = 1<<2, NETPLAN_OPTIONAL_DHCP6 = 1<<3, NETPLAN_OPTIONAL_STATIC = 1<<4, } NetplanOptionalAddressFlag; /* Fields below are valid for dhcp4 and dhcp6 unless otherwise noted. */ typedef struct dhcp_overrides { gboolean use_dns; gboolean use_ntp; gboolean send_hostname; gboolean use_hostname; gboolean use_mtu; gboolean use_routes; char* use_domains; /* netplan-feature: dhcp-use-domains */ char* hostname; guint metric; } NetplanDHCPOverrides; typedef enum { NETPLAN_RA_MODE_KERNEL, NETPLAN_RA_MODE_ENABLED, NETPLAN_RA_MODE_DISABLED, } NetplanRAMode; typedef enum { NETPLAN_IB_MODE_KERNEL, NETPLAN_IB_MODE_DATAGRAM, NETPLAN_IB_MODE_CONNECTED, NETPLAN_IB_MODE_MAX_, } NetplanInfinibandMode; typedef enum { NETPLAN_WIFI_WOWLAN_DEFAULT = 1<<0, NETPLAN_WIFI_WOWLAN_ANY = 1<<1, NETPLAN_WIFI_WOWLAN_DISCONNECT = 1<<2, NETPLAN_WIFI_WOWLAN_MAGIC = 1<<3, NETPLAN_WIFI_WOWLAN_GTK_REKEY_FAILURE = 1<<4, NETPLAN_WIFI_WOWLAN_EAP_IDENTITY_REQ = 1<<5, NETPLAN_WIFI_WOWLAN_4WAY_HANDSHAKE = 1<<6, NETPLAN_WIFI_WOWLAN_RFKILL_RELEASE = 1<<7, NETPLAN_WIFI_WOWLAN_TCP = 1<<8, } NetplanWifiWowlanFlag; struct NetplanWifiWowlanType { char* name; NetplanWifiWowlanFlag flag; }; /* Tunnel mode enum; sync with NetworkManager's DBUS API */ /* TODO: figure out whether networkd's GRETAP and NM's ISATAP * are the same thing. */ typedef enum { NETPLAN_TUNNEL_MODE_UNKNOWN = 0, NETPLAN_TUNNEL_MODE_IPIP = 1, NETPLAN_TUNNEL_MODE_GRE = 2, NETPLAN_TUNNEL_MODE_SIT = 3, NETPLAN_TUNNEL_MODE_ISATAP = 4, // NM only. NETPLAN_TUNNEL_MODE_VTI = 5, NETPLAN_TUNNEL_MODE_IP6IP6 = 6, NETPLAN_TUNNEL_MODE_IPIP6 = 7, NETPLAN_TUNNEL_MODE_IP6GRE = 8, NETPLAN_TUNNEL_MODE_VTI6 = 9, NETPLAN_TUNNEL_MODE_GRETAP = 10, NETPLAN_TUNNEL_MODE_IP6GRETAP = 11, /* "ip-tunnel" modes supported by Network Manager end here */ NETPLAN_TUNNEL_MODE_NM_MAX = 12, NETPLAN_TUNNEL_MODE_VXLAN = 100, NETPLAN_TUNNEL_MODE_WIREGUARD = 101, NETPLAN_TUNNEL_MODE_MAX_, } NetplanTunnelMode; typedef enum { NETPLAN_AUTH_KEY_MANAGEMENT_NONE, NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK, NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP, NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSHA256, NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSUITE_B_192, NETPLAN_AUTH_KEY_MANAGEMENT_8021X, NETPLAN_AUTH_KEY_MANAGEMENT_WPA_SAE, NETPLAN_AUTH_KEY_MANAGEMENT_MAX, } NetplanAuthKeyManagementType; typedef enum { NETPLAN_AUTH_EAP_NONE, NETPLAN_AUTH_EAP_TLS, NETPLAN_AUTH_EAP_PEAP, NETPLAN_AUTH_EAP_TTLS, NETPLAN_AUTH_EAP_LEAP, NETPLAN_AUTH_EAP_PWD, NETPLAN_AUTH_EAP_UNKNOWN, NETPLAN_AUTH_EAP_METHOD_MAX, } NetplanAuthEAPMethod; typedef enum { NETPLAN_AUTH_PMF_MODE_NONE, NETPLAN_AUTH_PMF_MODE_DISABLED, NETPLAN_AUTH_PMF_MODE_OPTIONAL, NETPLAN_AUTH_PMF_MODE_REQUIRED, } NetplanAuthPMFMode; typedef struct authentication_settings { NetplanAuthKeyManagementType key_management; NetplanAuthEAPMethod eap_method; NetplanAuthPMFMode pmf_mode; char* identity; char* anonymous_identity; char* password; char* ca_certificate; char* client_certificate; char* client_key; char* client_key_password; char* phase2_auth; /* netplan-feature: auth-phase2 */ char* psk; } NetplanAuthenticationSettings; typedef enum { NETPLAN_KEY_FLAG_NONE = 0, NETPLAN_KEY_FLAG_AGENT_OWNED = 1<<0, NETPLAN_KEY_FLAG_NOT_SAVED = 1<<1, NETPLAN_KEY_FLAG_NOT_REQUIRED = 1<<2, NETPLAN_KEY_FLAG_MAX_ } NetplanKeyFlags; typedef struct ovs_controller { char* connection_mode; GArray* addresses; } NetplanOVSController; typedef struct ovs_settings { GHashTable* external_ids; GHashTable* other_config; char* lacp; char* fail_mode; gboolean mcast_snooping; GArray* protocols; gboolean rstp; NetplanOVSController controller; NetplanAuthenticationSettings ssl; } NetplanOVSSettings; typedef struct netplan_backend_settings { char *name; char *uuid; char *stable_id; char *device; GData* passthrough; /* See g_datalist* functions */ } NetplanBackendSettings; typedef enum { /** * @brief Tristate enum type * * This type defines a boolean which can be unset, i.e. * this type has three states. The enum is ordered so * that * * UNSET -> -1 * FALSE -> 0 * TRUE -> 1 * * And the integer values can be used directly when * converting to string. */ NETPLAN_TRISTATE_UNSET = -1, /* -1 */ NETPLAN_TRISTATE_FALSE, /* 0 */ NETPLAN_TRISTATE_TRUE, /* 1 */ } NetplanTristate; typedef struct netplan_vxlan NetplanVxlan; /* Keep 'struct netplan_net_definition' in a separate header file, to allow for * abidiff to consider it "public API" (although it isn't) and notify us about * ABI compatibility issues. */ struct netplan_net_definition { NetplanDefType type; NetplanBackend backend; char* id; /* only necessary for NetworkManager connection UUIDs in some cases */ uuid_t uuid; /* status options */ gboolean optional; NetplanOptionalAddressFlag optional_addresses; gboolean critical; /* addresses */ gboolean dhcp4; gboolean dhcp6; char* dhcp_identifier; NetplanDHCPOverrides dhcp4_overrides; NetplanDHCPOverrides dhcp6_overrides; NetplanRAMode accept_ra; GArray* ip4_addresses; GArray* ip6_addresses; GArray* address_options; gboolean ip6_privacy; guint ip6_addr_gen_mode; char* ip6_addr_gen_token; char* gateway4; char* gateway6; GArray* ip4_nameservers; GArray* ip6_nameservers; GArray* search_domains; GArray* routes; GArray* ip_rules; GArray* wireguard_peers; struct { gboolean ipv4; gboolean ipv6; } linklocal; /* primary ID for member devices */ char* bridge; // deprecated, use bridge_link instead char* bond; // deprecated, use bond_link instead /* peer ID for OVS patch ports */ char* peer; // deprecated, use peer_link instead /* vlan */ guint vlan_id; NetplanNetDefinition* vlan_link; gboolean has_vlans; /* Configured custom MAC address */ char* set_mac; /* interface mtu */ guint mtubytes; /* ipv6 mtu */ /* netplan-feature: ipv6-mtu */ guint ipv6_mtubytes; /* these properties are only valid for physical interfaces (type < ND_VIRTUAL) */ char* set_name; struct { /* A glob (or tab-separated list of globs) to match a specific driver */ char* driver; char* mac; char* original_name; } match; gboolean has_match; gboolean wake_on_lan; gint wowlan; gboolean emit_lldp; /* these properties are only valid for NETPLAN_DEF_TYPE_WIFI */ GHashTable* access_points; /* SSID → NetplanWifiAccessPoint* */ struct { char* mode; char* lacp_rate; char* monitor_interval; guint min_links; char* transmit_hash_policy; char* selection_logic; gboolean all_members_active; char* arp_interval; GArray* arp_ip_targets; char* arp_validate; char* arp_all_targets; char* up_delay; char* down_delay; char* fail_over_mac_policy; guint gratuitous_arp; /* TODO: unsolicited_na */ guint packets_per_member; char* primary_reselect_policy; guint resend_igmp; char* learn_interval; char* primary_member; } bond_params; /* netplan-feature: modems */ struct { char* apn; gboolean auto_config; char* device_id; char* network_id; char* number; char* password; char* pin; char* sim_id; char* sim_operator_id; char* username; } modem_params; struct { char* ageing_time; guint priority; guint port_priority; char* forward_delay; char* hello_time; char* max_age; guint path_cost; gboolean stp; } bridge_params; gboolean custom_bridging; struct { NetplanTunnelMode mode; char *local_ip; char *remote_ip; char *input_key; char *output_key; char *private_key; /* used for wireguard */ guint fwmark; guint port; } tunnel; NetplanAuthenticationSettings auth; gboolean has_auth; /* these properties are only valid for SR-IOV NICs */ /* netplan-feature: sriov */ NetplanNetDefinition* sriov_link; gboolean sriov_vlan_filter; guint sriov_explicit_vf_count; /* these properties are only valid for OpenVSwitch */ /* netplan-feature: openvswitch */ NetplanOVSSettings ovs_settings; NetplanBackendSettings backend_settings; char* filepath; /* it cannot be in the tunnel struct: https://github.com/canonical/netplan/pull/206 */ guint tunnel_ttl; /* netplan-feature: activation-mode */ char* activation_mode; /* configure without carrier */ gboolean ignore_carrier; /* offload options */ NetplanTristate receive_checksum_offload; NetplanTristate transmit_checksum_offload; NetplanTristate tcp_segmentation_offload; NetplanTristate tcp6_segmentation_offload; NetplanTristate generic_segmentation_offload; NetplanTristate generic_receive_offload; NetplanTristate large_receive_offload; struct private_netdef_data* _private; /* netplan-feature: eswitch-mode */ char* embedded_switch_mode; gboolean sriov_delay_virtual_functions_rebind; /* netplan-feature: infiniband */ NetplanInfinibandMode ib_mode; /* IPoIB */ /* netplan-feature: regdom */ char* regulatory_domain; /* vrf */ /* netplan-feature: vrf */ NetplanNetDefinition* vrf_link; guint vrf_table; NetplanTristate bridge_neigh_suppress; /* vxlan */ /* netplan-feature: vxlan */ gboolean has_vxlans; NetplanVxlan* vxlan; NetplanNetDefinition* bridge_link; NetplanNetDefinition* bond_link; NetplanNetDefinition* peer_link; /* True if "networkmanager" settings are present */ gboolean has_backend_settings_nm; guint tunnel_private_key_flags; /* virtual-ethernet */ /* netplan-feature: virtual-ethernet */ NetplanNetDefinition* veth_peer_link; NetplanTristate bridge_hairpin; NetplanTristate bridge_learning; }; netplan-1.0/src/dbus.c000066400000000000000000000764471457004145200147270ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "_features.h" #include "util-internal.h" typedef struct { sd_bus_slot *slot; gboolean invalidated; } NetplanConfigData; typedef struct { sd_bus *bus; sd_event_source *try_es; GPid try_pid; /* semaphore. There can only be one 'netplan try' child process at a time */ const char *config_id; /* current config ID, during any io.netplan.Netplan.Config calls */ char *handler_id; /* copy of pending config ID, during io.netplan.Netplan.Config.Try() */ char *config_dirty; /* Currently pending Set() config object id */ GHashTable *config_data; /* data of to the /io/netplan/Netplan/config/ objects */ } NetplanData; static const char* NETPLAN_SUBDIRS[3] = {"etc", "run", "lib"}; static const char* NETPLAN_GLOBAL_CONFIG = "BACKUP"; static char* NETPLAN_ROOT = "/"; /* Can be modified for testing netplan-dbus */ static void invalidate_other_config(gpointer key, gpointer value, gpointer user_data) { const char *id = key; const char *current_config_id = user_data; NetplanConfigData *cd = value; if (current_config_id == NULL) cd->invalidated = FALSE; else if (g_strcmp0(id, current_config_id)) cd->invalidated = TRUE; } static int terminate_try_child_process(int status, NetplanData *d, const char *config_id) { sd_bus_message *msg = NULL; g_autofree gchar *path = NULL; int r = 0; if (!WIFEXITED(status)) fprintf(stderr, "'netplan try' exited with status: %d\n", WEXITSTATUS(status)); // LCOV_EXCL_LINE /* Cleanup current 'netplan try' child process */ sd_event_source_unref(d->try_es); d->try_es = NULL; g_spawn_close_pid (d->try_pid); d->try_pid = -1; /* unlock semaphore */ /* Send .Changed() signal on DBus */ if (config_id) { path = g_strdup_printf("/io/netplan/Netplan/config/%s", config_id); r = sd_bus_message_new_signal(d->bus, &msg, path, "io.netplan.Netplan.Config", "Changed"); } if (r < 0) { // LCOV_EXCL_START fprintf(stderr, "Could not create .Changed() signal: %s\n", strerror(-r)); return r; // LCOV_EXCL_STOP } r = sd_bus_send(d->bus, msg, NULL); if (r < 0) fprintf(stderr, "Could not send .Changed() signal: %s\n", strerror(-r)); // LCOV_EXCL_LINE sd_bus_message_unrefp(&msg); return r; } static int _try_accept(bool accept, sd_bus_message *m, NetplanData *d, sd_bus_error *ret_error) { g_autoptr(GError) error = NULL; int status = -1; int signal = SIGUSR1; if (!accept) signal = SIGINT; /* Do not send the accept/reject signal, if this call is for another config state */ if (d->handler_id != NULL && g_strcmp0(d->config_id, d->handler_id)) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Another 'netplan try' process is already running"); /* ATTENTION: There might be a race here: * When this accept/reject method is called at the same time as the 'netplan try' * python process is reverting and closing itself. Not sure what to do about it... * Maybe this needs to be fixed in python code, so that the * 'netplan.terminal.InputRejected' exception (i.e. self-revert) cannot be * interrupted by another exception/signal */ /* Send confirm (SIGUSR1) or cancel (SIGINT) signal to 'netplan try' process. * Wait for the child process to stop, synchronously. * Check return code/errors. */ kill(d->try_pid, signal); waitpid(d->try_pid, &status, 0); #if GLIB_CHECK_VERSION (2, 70, 0) g_spawn_check_wait_status(status, &error); #else g_spawn_check_exit_status(status, &error); #endif if (error != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan try failed: %s", error->message); // LCOV_EXCL_LINE terminate_try_child_process(status, d, d->config_id); return sd_bus_reply_method_return(m, "b", true); } static int _copy_yaml_state(char *src_root, char *dst_root, sd_bus_error *ret_error) { glob_t gl; g_autoptr(GError) err = NULL; int r = _netplan_find_yaml_glob(src_root, &gl); if (!!r) // LCOV_EXCL_START return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Failed glob for YAML files\n"); // LCOV_EXCL_STOP /* Copy all *.yaml files from "/SRC_ROOT/{etc,run,lib}/netplan/" to * "/DST_ROOT/{etc,run,lib}/netplan/" */ GFile *source = NULL; GFile *dest = NULL; gchar *dest_path = NULL; size_t len = strlen(src_root); for (size_t i = 0; i < gl.gl_pathc; ++i) { dest_path = g_build_path(G_DIR_SEPARATOR_S, dst_root, (gl.gl_pathv[i])+len, NULL); source = g_file_new_for_path(gl.gl_pathv[i]); dest = g_file_new_for_path(dest_path); g_file_copy(source, dest, G_FILE_COPY_OVERWRITE |G_FILE_COPY_NOFOLLOW_SYMLINKS |G_FILE_COPY_ALL_METADATA, NULL, NULL, NULL, &err); if (err != NULL) { // LCOV_EXCL_START r = sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Failed to copy file %s -> %s: %s\n", g_file_get_path(source), g_file_get_path(dest), err->message); g_object_unref(source); g_object_unref(dest); g_free(dest_path); globfree(&gl); return r; // LCOV_EXCL_STOP } g_object_unref(source); g_object_unref(dest); g_free(dest_path); } globfree(&gl); return r; } static bool _clear_tmp_state(const char *config_id, NetplanData *d) { g_autofree gchar *rootdir = NULL; /* Remove tmp YAML files */ rootdir = g_strdup_printf("%s/run/netplan/config-%s", NETPLAN_ROOT, config_id); _netplan_unlink_glob(rootdir, "/{etc,run,lib}/netplan/*.yaml"); /* Remove tmp state directories */ char *subdir = NULL; for (int i = 0; i < 3; i++) { subdir = g_strdup_printf("%s/%s/netplan", rootdir, NETPLAN_SUBDIRS[i]); rmdir(subdir); g_free(subdir); subdir = g_strdup_printf("%s/%s", rootdir, NETPLAN_SUBDIRS[i]); rmdir(subdir); g_free(subdir); } rmdir(rootdir); /* No cleanup of DBus object needed, if config_id points to NETPLAN_GLOBAL_CONFIG (backup) */ if (config_id != NETPLAN_GLOBAL_CONFIG) { /* Clear config object from DBus, by unref the appropriate slot */ NetplanConfigData *cd = g_hash_table_lookup(d->config_data, config_id); sd_bus_slot_unref(cd->slot); /* Clear value/slot */ g_free(cd); /* Clear value/struct */ g_hash_table_remove(d->config_data, config_id); /* Clear key */ d->config_dirty = NULL; /* TODO: HashTable error handling */ } return TRUE; } static int _backup_global_state(sd_bus_error *ret_error) { int r = 0; g_autofree gchar *path = NULL; path = g_strdup_printf("%s/run/netplan/config-%s", NETPLAN_ROOT, NETPLAN_GLOBAL_CONFIG); /* Create {etc,run,lib} subdirs with owner r/w permissions */ char *subdir = NULL; for (int i = 0; i < 3; i++) { subdir = g_strdup_printf("%s/%s/netplan", path, NETPLAN_SUBDIRS[i]); r = g_mkdir_with_parents(subdir, 0700); if (r < 0) // LCOV_EXCL_START return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Failed to create '%s': %s\n", subdir, strerror(errno)); // LCOV_EXCL_STOP g_free(subdir); } /* Copy main *.yaml files from /{etc,run,lib}/netplan/ to GLOBAL backup dir */ r = _copy_yaml_state(NETPLAN_ROOT, path, ret_error); return r; } /** * io.netplan.Netplan methods */ static int method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { g_autoptr(GError) err = NULL; g_autofree gchar *stdout = NULL; g_autofree gchar *stderr = NULL; g_autofree gchar *state = NULL; gint exit_status = 0; NetplanData *d = userdata; /* Accept the current 'netplan try', if active. * Otherwise execute 'netplan apply' directly. */ if (d->try_pid > 0) return _try_accept(TRUE, m, userdata, ret_error); if (d->config_id) state = g_strdup_printf("--state=%s/run/netplan/config-%s", NETPLAN_ROOT, NETPLAN_GLOBAL_CONFIG); gchar *argv[] = {SBINDIR "/" "netplan", "apply", state, NULL}; // for tests only: allow changing what netplan to run if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdout, &stderr, &exit_status, &err); // LCOV_EXCL_START if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan apply: %s", err->message); #if GLIB_CHECK_VERSION (2, 70, 0) g_spawn_check_wait_status(exit_status, &err); #else g_spawn_check_exit_status(exit_status, &err); #endif if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan apply failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_STOP return sd_bus_reply_method_return(m, "b", true); } static int method_generate(sd_bus_message *m, __unused void *userdata, sd_bus_error *ret_error) { g_autoptr(GError) err = NULL; g_autofree gchar *stdout = NULL; g_autofree gchar *stderr = NULL; gint exit_status = 0; gchar *argv[] = {SBINDIR "/" "netplan", "generate", NULL}; // for tests only: allow changing what netplan to run if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdout, &stderr, &exit_status, &err); // LCOV_EXCL_START if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan generate: %s", err->message); #if GLIB_CHECK_VERSION (2, 70, 0) g_spawn_check_wait_status(exit_status, &err); #else g_spawn_check_exit_status(exit_status, &err); #endif if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan generate failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_STOP return sd_bus_reply_method_return(m, "b", true); } static int method_info(sd_bus_message *m, __unused void *userdata, __unused sd_bus_error *ret_error) { sd_bus_message *reply = NULL; gint exit_status = 0; exit_status = sd_bus_message_new_method_return(m, &reply); if (exit_status < 0) return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_open_container(reply, 'a', "(sv)"); if (exit_status < 0) return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_open_container(reply, 'r', "sv"); if (exit_status < 0) return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_append(reply, "s", "Features"); if (exit_status < 0) return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_open_container(reply, 'v', "as"); if (exit_status < 0) return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_append_strv(reply, (char**)feature_flags); if (exit_status < 0) return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_close_container(reply); if (exit_status < 0) return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_close_container(reply); if (exit_status < 0) return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_close_container(reply); if (exit_status < 0) return exit_status; // LCOV_EXCL_LINE return sd_bus_send(NULL, reply, NULL); } static int method_get(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { NetplanData *d = userdata; g_autoptr(GError) err = NULL; g_autofree gchar *stdout = NULL; g_autofree gchar *stderr = NULL; g_autofree gchar *root_dir = NULL; gint exit_status = 0; if (d->config_id) root_dir = g_strdup_printf("--root-dir=%s/run/netplan/config-%s", NETPLAN_ROOT, d->config_id); gchar *argv[] = {SBINDIR "/" "netplan", "get", "all", root_dir, NULL}; // for tests only: allow changing what netplan to run if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdout, &stderr, &exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan get: %s", err->message); // LCOV_EXCL_LINE #if GLIB_CHECK_VERSION (2, 70, 0) g_spawn_check_wait_status(exit_status, &err); #else g_spawn_check_exit_status(exit_status, &err); #endif if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan get failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE return sd_bus_reply_method_return(m, "s", stdout); } static int method_set(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { NetplanData *d = userdata; g_autoptr(GError) err = NULL; g_autofree gchar *stdout = NULL; g_autofree gchar *stderr = NULL; g_autofree gchar *origin = NULL; g_autofree gchar *root_dir = NULL; gint exit_status = 0; char *args[2] = {NULL, NULL}; char *config_delta = NULL; char *origin_hint = NULL; guint cur_arg = 0; if (sd_bus_message_read(m, "ss", &config_delta, &origin_hint) < 0) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot extract config_delta or origin_hint"); // LCOV_EXCL_LINE if (!!strcmp(origin_hint, "")) { origin = g_strdup_printf("--origin-hint=%s", origin_hint); args[cur_arg] = origin; cur_arg++; } if (d->config_id) { root_dir = g_strdup_printf("--root-dir=%s/run/netplan/config-%s", NETPLAN_ROOT, d->config_id); args[cur_arg] = root_dir; cur_arg++; } gchar *argv[] = {SBINDIR "/" "netplan", "set", config_delta, args[0], args[1], NULL}; // for tests only: allow changing what netplan to run if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdout, &stderr, &exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan set %s: %s", config_delta, err->message); // LCOV_EXCL_LINE #if GLIB_CHECK_VERSION (2, 70, 0) g_spawn_check_wait_status(exit_status, &err); #else g_spawn_check_exit_status(exit_status, &err); #endif if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan set failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE return sd_bus_reply_method_return(m, "b", true); } static int netplan_try_cancelled_cb(__unused sd_event_source *es, const siginfo_t *si, void* userdata) { NetplanData *d = userdata; g_autofree gchar *state_dir = NULL; int r = 0; if (d->handler_id) { /* Delete GLOBAL state */ _netplan_unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml"); /* Restore GLOBAL backup config state to main rootdir */ state_dir = g_strdup_printf("%s/run/netplan/config-%s", NETPLAN_ROOT, NETPLAN_GLOBAL_CONFIG); r = _copy_yaml_state(state_dir, NETPLAN_ROOT, NULL); if (r < 0) return r; /* Un-invalidate all other current config objects */ if (!g_strcmp0(d->handler_id, d->config_dirty)) g_hash_table_foreach(d->config_data, invalidate_other_config, NULL); /* Clear GLOBAL backup and config state */ _clear_tmp_state(NETPLAN_GLOBAL_CONFIG, d); _clear_tmp_state(d->handler_id, d); } r = terminate_try_child_process(si->si_status, d, d->handler_id); /* free and reset handler_id, i.e. copy of config state ID */ g_free(d->handler_id); d->handler_id = NULL; /* unlock pending config ID */ return r; } static int method_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { g_autoptr(GError) err = NULL; g_autofree gchar *timeout = NULL; g_autofree gchar *state = NULL; g_autofree gchar *netplan_try_stamp = NULL; struct stat buf; gint child_stdin = -1; /* child process needs an input to function correctly */ guint seconds = 0; int r = -1; NetplanData *d = userdata; if (sd_bus_message_read_basic (m, 'u', &seconds) < 0) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot extract timeout_seconds"); // LCOV_EXCL_LINE if (seconds > 0) timeout = g_strdup_printf("--timeout=%u", seconds); if (d->config_id) state = g_strdup_printf("--state=%s/run/netplan/config-%s", NETPLAN_ROOT, NETPLAN_GLOBAL_CONFIG); gchar *argv[] = {SBINDIR "/" "netplan", "try", timeout, state, NULL}; // for tests only: allow changing what netplan to run if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); /* Delete any left-over netplan-try.ready stamp file, if it exists */ netplan_try_stamp = g_build_path("/", NETPLAN_ROOT, "run", "netplan", "netplan-try.ready", NULL); unlink(netplan_try_stamp); /* Launch 'netplan try' child process, lock 'try_pid' to real PID */ g_spawn_async_with_pipes("/", argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD|G_SPAWN_STDOUT_TO_DEV_NULL, NULL, NULL, &d->try_pid, &child_stdin, NULL, NULL, &err); if (err) // LCOV_EXCL_START return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan try: %s", err->message); // LCOV_EXCL_STOP /* Register an event handler, trigged when the child process exits */ if (d->config_id) d->handler_id = g_strdup(d->config_id); /* to free in event handler */ r = sd_event_add_child(sd_bus_get_event(d->bus), &d->try_es, d->try_pid, WEXITED, netplan_try_cancelled_cb, d); if (r < 0) // LCOV_EXCL_START return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot watch 'netplan try' child: %s", strerror(-r)); // LCOV_EXCL_STOP /* wait for the /run/netplan/netplan-try.ready stamp file to appear */ guint poll_timeout = 1000; /* Replace the default timeout with the one specified by the caller */ if (seconds > 0) poll_timeout = seconds * 100; /* Timeout after up to 10 sec of waiting for the stamp file */ for (guint i = 0; i < poll_timeout; i++) { struct timespec timeout = { .tv_sec = 0, .tv_nsec = 1000 * 1000 * 10, // 10 ms }; if (stat(netplan_try_stamp, &buf) == 0) break; nanosleep(&timeout, NULL); } if (stat(netplan_try_stamp, &buf) != 0) { g_debug("cannot find %s stamp file", netplan_try_stamp); return sd_bus_reply_method_return(m, "b", false); } return sd_bus_reply_method_return(m, "b", true); } /** * io.netplan.Netplan.Config methods */ /* netplan-feature: dbus-config */ static int method_config_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { NetplanData *d = userdata; g_autofree gchar *state_dir = NULL; int r = 0; /* trim 27 chars (i.e. "/io/netplan/Netplan/config/") from path to get the config ID */ d->config_id = sd_bus_message_get_path(m) + 27; NetplanConfigData *cd = g_hash_table_lookup(d->config_data, d->config_id); if (cd->invalidated) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "This config was invalidated by another config object\n"); /* Invalidate all other current config objects */ g_hash_table_foreach(d->config_data, invalidate_other_config, (void*)d->config_id); d->config_dirty = g_strdup(d->config_id); if (d->try_pid < 0) { r = _backup_global_state(ret_error); if (r < 0) return r; // LCOV_EXCL_LINE /* Delete GLOBAL state */ _netplan_unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml"); /* Copy current config state to GLOBAL */ state_dir = g_strdup_printf("%s/run/netplan/config-%s", NETPLAN_ROOT, d->config_id); r = _copy_yaml_state(state_dir, NETPLAN_ROOT, ret_error); if (r < 0) return r; d->handler_id = g_strdup(d->config_id); } r = method_apply(m, d, ret_error); /* Clear GLOBAL backup and config state */ _clear_tmp_state(NETPLAN_GLOBAL_CONFIG, d); _clear_tmp_state(d->config_id, d); /* unlock current config ID and handler ID */ d->config_id = NULL; g_free(d->handler_id); d->handler_id = NULL; return r; } static int method_config_get(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { NetplanData *d = userdata; /* trim 27 chars (i.e. "/io/netplan/Netplan/config/") from path to get the config ID */ d->config_id = sd_bus_message_get_path(m) + 27; int r = method_get(m, userdata, ret_error); /* Reset config_id for next method call */ d->config_id = NULL; return r; } static int method_config_set(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { NetplanData *d = userdata; /* trim 27 chars (i.e. "/io/netplan/Netplan/config/") from path to get the config ID */ d->config_id = sd_bus_message_get_path(m) + 27; NetplanConfigData *cd = g_hash_table_lookup(d->config_data, d->config_id); if (cd->invalidated) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "This config was invalidated by another config object\n"); int r = method_set(m, d, ret_error); /* Invalidate all other current config objects */ g_hash_table_foreach(d->config_data, invalidate_other_config, (void*)d->config_id); d->config_dirty = g_strdup(d->config_id); /* Reset config_id for next method call */ d->config_id = NULL; return r; } static int method_config_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { NetplanData *d = userdata; g_autofree gchar *state_dir = NULL; const char *config_id = sd_bus_message_get_path(m) + 27; if (d->try_pid > 0) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Another Try() is currently in progress: PID %d\n", d->try_pid); NetplanConfigData *cd = g_hash_table_lookup(d->config_data, config_id); if (cd->invalidated) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "This config was invalidated by another config object\n"); int r = 0; /* Lock current child process temporarily until we have a real PID */ d->try_pid = G_MAXINT; d->config_id = config_id; r = _backup_global_state(ret_error); if (r < 0) return r; // LCOV_EXCL_LINE /* Clear main *.yaml files */ _netplan_unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml"); /* Copy current config *.yaml state to main rootdir (i.e. /etc/netplan/) */ state_dir = g_strdup_printf("%s/run/netplan/config-%s", NETPLAN_ROOT, d->config_id); r = _copy_yaml_state(state_dir, NETPLAN_ROOT, ret_error); if (r < 0) return r; /* Exec try */ r = method_try(m, userdata, ret_error); d->config_id = NULL; return r; } static int method_config_cancel(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { NetplanData *d = userdata; g_autofree gchar *state_dir = NULL; int r = 0; /* trim 27 chars (i.e. "/io/netplan/Netplan/config/") from path to get the config ID */ d->config_id = sd_bus_message_get_path(m) + 27; if (!g_strcmp0(d->config_id, d->config_dirty)) /* Un-invalidate all other current config objects */ g_hash_table_foreach(d->config_data, invalidate_other_config, NULL); /* Cancel the current 'netplan try' process */ if (d->try_pid > 0) r = _try_accept(FALSE, m, d, ret_error); else r = sd_bus_reply_method_return(m, "b", true); if (d->handler_id && !g_strcmp0(d->config_id, d->handler_id)) { /* Delete GLOBAL state */ _netplan_unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml"); /* Restore GLOBAL backup config state to main rootdir */ state_dir = g_strdup_printf("%s/run/netplan/config-%s", NETPLAN_ROOT, NETPLAN_GLOBAL_CONFIG); r = _copy_yaml_state(state_dir, NETPLAN_ROOT, ret_error); if (r < 0) return r; /* Clear GLOBAL backup and config state */ _clear_tmp_state(NETPLAN_GLOBAL_CONFIG, d); /* Clear pending Try() handler ID */ g_free(d->handler_id); d->handler_id = NULL; } /* Clear tmp state */ _clear_tmp_state(d->config_id, d); d->config_id = NULL; return r; } static const sd_bus_vtable config_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("Apply", "", "b", method_config_apply, 0), SD_BUS_METHOD("Get", "", "s", method_config_get, 0), SD_BUS_METHOD("Set", "ss", "b", method_config_set, 0), SD_BUS_METHOD("Try", "u", "b", method_config_try, 0), SD_BUS_METHOD("Cancel", "", "b", method_config_cancel, 0), SD_BUS_VTABLE_END }; /** * Link between io.netplan.Netplan and io.netplan.Netplan.Config */ static int method_config(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { NetplanData *d = userdata; sd_bus_slot *slot = NULL; g_autoptr(GError) err = NULL; g_autofree gchar *tmpl = NULL; g_autofree gchar *dir = NULL; gchar *path = NULL; int r = 0; /* Create state directory, according to "run/netplan/config-XXXXXX" template */ tmpl = g_build_path("/", NETPLAN_ROOT, "run", "netplan", "config-XXXXXX", NULL); dir = g_path_get_dirname(tmpl); r = g_mkdir_with_parents(dir, 0700); path = g_mkdtemp(tmpl); // returns pointer to tmpl (with modified string) if (r < 0 || !path) // LCOV_EXCL_START return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Failed to create temp dir: %s\n", strerror(errno)); // LCOV_EXCL_STOP /* Extract the last 6 randomly generated chars (i.e. "XXXXXX" from template) */ const char *id = path + strlen(path) - 6; const char *obj_path = g_strdup_printf("/io/netplan/Netplan/config/%s", id); r = sd_bus_add_object_vtable(d->bus, &slot, obj_path, "io.netplan.Netplan.Config", config_vtable, d); // LCOV_EXCL_START if (r < 0) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Failed to add 'config' object: %s\n", strerror(-r)); NetplanConfigData *cd = g_new0(NetplanConfigData, 1); cd->slot = slot; /* Cannot Set()/Apply() if another Set() is currently pending */ cd->invalidated = d->config_dirty ? TRUE : FALSE; if (!g_hash_table_insert(d->config_data, g_strdup(id), cd)) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Failed to add object data to HashTable\n"); // LCOV_EXCL_STOP /* Create {etc,run,lib} subdirs with owner r/w permissions */ char *subdir = NULL; for (int i = 0; i < 3; i++) { subdir = g_strdup_printf("%s/%s/netplan", path, NETPLAN_SUBDIRS[i]); r = g_mkdir_with_parents(subdir, 0700); if (r < 0) // LCOV_EXCL_START return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Failed to create '%s': %s\n", subdir, strerror(errno)); // LCOV_EXCL_STOP g_free(subdir); } /* Copy all *.yaml files from /{etc,run,lib}/netplan/ to temp dir */ r = _copy_yaml_state(NETPLAN_ROOT, path, ret_error); if (r < 0) return r; return sd_bus_reply_method_return(m, "o", obj_path); } static const sd_bus_vtable netplan_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("Apply", "", "b", method_apply, 0), SD_BUS_METHOD("Generate", "", "b", method_generate, 0), SD_BUS_METHOD("Info", "", "a(sv)", method_info, 0), SD_BUS_METHOD("Config", "", "o", method_config, 0), SD_BUS_VTABLE_END }; /** * DBus setup */ static int terminate_mainloop_cb(__unused sd_event_source *es, __unused const struct signalfd_siginfo *si, void* userdata) { sd_event *event = userdata; /* Gracefully terminate the mainloop, to write GCOV output */ sd_event_exit(event, 0); return 0; } int main(__unused int argc, __unused char *argv[]) { sd_bus_slot *slot = NULL; sd_bus *bus = NULL; sd_event *event = NULL; NetplanData *data = g_new0(NetplanData, 1); sigset_t mask; int r; // for tests only: allow changing which rootdir to use to copy files around if (getenv("DBUS_TEST_NETPLAN_ROOT") != 0) NETPLAN_ROOT = getenv("DBUS_TEST_NETPLAN_ROOT"); /* TODO: consider sd_bus_default(&bus) for easier testing on session/user bus */ r = sd_bus_open_system(&bus); if (r < 0) { // LCOV_EXCL_START fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r)); goto finish; // LCOV_EXCL_STOP } r = sd_event_new(&event); if (r < 0) { // LCOV_EXCL_START fprintf(stderr, "Failed to create event loop: %s\n", strerror(-r)); goto finish; // LCOV_EXCL_STOP } /* Initialize the userdata */ data->bus = bus; data->try_pid = -1; data->config_id = NULL; data->handler_id = NULL; data->config_dirty = NULL; /* TODO: define a proper free/cleanup function for sd_bus_slot_unref() */ data->config_data = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); r = sd_bus_add_object_vtable(bus, &slot, "/io/netplan/Netplan", /* object path */ "io.netplan.Netplan", /* interface name */ netplan_vtable, data); if (r < 0) { // LCOV_EXCL_START fprintf(stderr, "Failed to issue method call: %s\n", strerror(-r)); goto finish; // LCOV_EXCL_STOP } r = sd_bus_request_name(bus, "io.netplan.Netplan", 0); if (r < 0) { fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-r)); goto finish; } r = sd_bus_attach_event(bus, event, SD_EVENT_PRIORITY_NORMAL); if (r < 0) { // LCOV_EXCL_START fprintf(stderr, "Failed to attach event loop: %s\n", strerror(-r)); goto finish; // LCOV_EXCL_STOP } /* Mask the SIGCHLD signal, so we can listen to it via mainloop */ sigemptyset(&mask); sigaddset(&mask, SIGCHLD); sigaddset(&mask, SIGTERM); sigprocmask(SIG_BLOCK, &mask, NULL); /* Start the event loop, wait for requests */ sd_event_add_signal(event, NULL, SIGTERM, terminate_mainloop_cb, event); r = sd_event_loop(event); if (r < 0) fprintf(stderr, "Failed mainloop: %s\n", strerror(-r)); // LCOV_EXCL_LINE finish: g_free(data); sd_event_unref(event); sd_bus_slot_unref(slot); sd_bus_unref(bus); /* TODO: unref all slots from HashTable */ return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; } netplan-1.0/src/error.c000066400000000000000000000144561457004145200151130ustar00rootroot00000000000000/* * Copyright (C) 2019 Canonical, Ltd. * Author: Mathieu Trudel-Lapierre * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include "util.h" #include "parse.h" #include "types-internal.h" #include "util-internal.h" /**************************************************** * Loading and error handling ****************************************************/ STATIC void write_error_marker(GString *message, int column) { int i; for (i = 0; (column > 0 && i < column); i++) g_string_append_printf(message, " "); g_string_append_printf(message, "^"); } STATIC char * get_syntax_error_context(const NetplanParser* npp, const int line_num, const int column, GError **error) { GString *message = NULL; GFile *cur_file = g_file_new_for_path(npp->current.filepath); GFileInputStream *file_stream; GDataInputStream *stream; gsize len; gchar* line = NULL; message = g_string_sized_new(200); file_stream = g_file_read(cur_file, NULL, error); stream = g_data_input_stream_new (G_INPUT_STREAM(file_stream)); g_object_unref(file_stream); for (int i = 0; i < line_num + 1; i++) { g_free(line); line = g_data_input_stream_read_line(stream, &len, NULL, error); } g_string_append_printf(message, "%s\n", line); g_free(line); write_error_marker(message, column); g_object_unref(stream); g_object_unref(cur_file); return g_string_free(message, FALSE); } STATIC char * get_parser_error_context(const yaml_parser_t *parser, __unused GError **error) { GString *message = NULL; unsigned char* line = parser->buffer.pointer; unsigned char* current = line; message = g_string_sized_new(200); while (current > parser->buffer.start) { current--; if (*current == '\n') { line = current + 1; break; } } if (current <= parser->buffer.start) line = parser->buffer.start; current = line + 1; while (current <= parser->buffer.last) { if (*current == '\n') { *current = '\0'; break; } current++; } g_string_append_printf(message, "%s\n", line); write_error_marker(message, parser->problem_mark.column); return g_string_free(message, FALSE); } gboolean parser_error(const yaml_parser_t* parser, const char* yaml, GError** error) { char *error_context = get_parser_error_context(parser, error); yaml = yaml ? yaml : "(unnamed file)"; if ((char)*parser->buffer.pointer == '\t') g_set_error(error, NETPLAN_PARSER_ERROR, NETPLAN_ERROR_INVALID_YAML, "%s:%zu:%zu: Invalid YAML: tabs are not allowed for indent:\n%s", yaml, parser->problem_mark.line + 1, parser->problem_mark.column + 1, error_context); else if (((char)*parser->buffer.pointer == ' ' || (char)*parser->buffer.pointer == '\0') && !parser->token_available) g_set_error(error, NETPLAN_PARSER_ERROR, NETPLAN_ERROR_INVALID_YAML, "%s:%zu:%zu: Invalid YAML: aliases are not supported:\n%s", yaml, parser->problem_mark.line + 1, parser->problem_mark.column + 1, error_context); else if (parser->state == YAML_PARSE_BLOCK_MAPPING_KEY_STATE) g_set_error(error, NETPLAN_PARSER_ERROR, NETPLAN_ERROR_INVALID_YAML, "%s:%zu:%zu: Invalid YAML: inconsistent indentation:\n%s", yaml, parser->problem_mark.line + 1, parser->problem_mark.column + 1, error_context); else { g_set_error(error, NETPLAN_PARSER_ERROR, NETPLAN_ERROR_INVALID_YAML, "%s:%zu:%zu: Invalid YAML: %s:\n%s", yaml, parser->problem_mark.line + 1, parser->problem_mark.column + 1, parser->problem, error_context); } g_free(error_context); return FALSE; } /** * Put a YAML specific error message for @node into @error. */ gboolean yaml_error(const NetplanParser *npp, const yaml_node_t* node, GError** error, const char* msg, ...) { va_list argp; char* s; char* error_context = NULL; va_start(argp, msg); g_vasprintf(&s, msg, argp); if (node != NULL && npp->current.filepath != NULL) { error_context = get_syntax_error_context(npp, node->start_mark.line, node->start_mark.column, error); g_set_error(error, NETPLAN_PARSER_ERROR, NETPLAN_ERROR_INVALID_CONFIG, "%s:%zu:%zu: Error in network definition: %s\n%s", npp->current.filepath, node->start_mark.line + 1, node->start_mark.column + 1, s, error_context); } else if (npp->current.filepath) { g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_VALIDATION, "%s: Error in network definition: %s", npp->current.filepath, s); } else { g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "Error in network definition: %s", s); } g_free(s); va_end(argp); g_free(error_context); return FALSE; } void netplan_error_clear(NetplanError** error) { g_clear_error(error); } ssize_t netplan_error_message(NetplanError* error, char* buf, size_t buf_size) { return netplan_copy_string(error->message, buf, buf_size); } uint64_t netplan_error_code(NetplanError* error) { uint64_t error_code = (uint64_t)error->domain << 32 | (uint64_t)error->code; return error_code; } netplan-1.0/src/error.h000066400000000000000000000017751457004145200151200ustar00rootroot00000000000000/* * Copyright (C) 2019 Canonical, Ltd. * Author: Mathieu Trudel-Lapierre * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "parse.h" gboolean parser_error(const yaml_parser_t* parser, const char* yaml, GError** error); gboolean yaml_error(const NetplanParser *npp, const yaml_node_t* node, GError** error, const char* msg, ...); netplan-1.0/src/generate.c000066400000000000000000000325261457004145200155520ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * Author: Martin Pitt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include /* Public API (from include/) */ #include "netplan.h" #include "parse.h" #include "util.h" /* Netplan internal (from src/) */ #include "names.h" #include "networkd.h" #include "nm.h" #include "openvswitch.h" #include "sriov.h" #include "util-internal.h" static gchar* rootdir; static gchar** files; static gboolean any_networkd = FALSE; static gboolean any_nm = FALSE; static gchar* mapping_iface; static GOptionEntry options[] = { {"root-dir", 'r', 0, G_OPTION_ARG_FILENAME, &rootdir, "Search for and generate configuration files in this root directory instead of /", NULL}, {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &files, "Read configuration from this/these file(s) instead of /etc/netplan/*.yaml", "[config file ..]"}, {"mapping", 0, 0, G_OPTION_ARG_STRING, &mapping_iface, "Only show the device to backend mapping for the specified interface.", NULL}, {NULL} }; static void reload_udevd(void) { const gchar *argv[] = { "/bin/udevadm", "control", "--reload", NULL }; g_spawn_sync(NULL, (gchar**)argv, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, NULL, NULL); }; /** * Create enablement symlink for systemd-networkd.service. */ static void enable_networkd(const char* generator_dir) { g_autofree char* link = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "multi-user.target.wants", "systemd-networkd.service", NULL); g_debug("We created networkd configuration, adding %s enablement symlink", link); _netplan_safe_mkdir_p_dir(link); if (symlink("../systemd-networkd.service", link) < 0 && errno != EEXIST) { // LCOV_EXCL_START g_fprintf(stderr, "failed to create enablement symlink: %m\n"); exit(1); // LCOV_EXCL_STOP } g_autofree char* link2 = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "network-online.target.wants", "systemd-networkd-wait-online.service", NULL); _netplan_safe_mkdir_p_dir(link2); if (symlink("/lib/systemd/system/systemd-networkd-wait-online.service", link2) < 0 && errno != EEXIST) { // LCOV_EXCL_START g_fprintf(stderr, "failed to create enablement symlink: %m\n"); exit(1); // LCOV_EXCL_STOP } } // LCOV_EXCL_START /* covered via 'cloud-init' integration test */ static gboolean check_called_just_in_time() { const gchar *argv[] = { "/bin/systemctl", "is-system-running", NULL }; gchar *output = NULL; g_spawn_sync(NULL, (gchar**)argv, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, &output, NULL, NULL, NULL); if (output != NULL && strstr(output, "initializing") != NULL) { g_free(output); const gchar *argv2[] = { "/bin/systemctl", "is-active", "network.target", NULL }; gint exit_code = 0; g_spawn_sync(NULL, (gchar**)argv2, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, &exit_code, NULL); /* return TRUE, if network.target is not yet active */ #if GLIB_CHECK_VERSION (2, 70, 0) return !g_spawn_check_wait_status(exit_code, NULL); #else return !g_spawn_check_exit_status(exit_code, NULL); #endif } g_free(output); return FALSE; }; static void start_unit_jit(gchar *unit) { const gchar *argv[] = { "/bin/systemctl", "start", "--no-block", "--no-ask-password", unit, NULL }; g_spawn_sync(NULL, (gchar**)argv, NULL, G_SPAWN_DEFAULT, NULL, NULL, NULL, NULL, NULL, NULL); }; // LCOV_EXCL_STOP static int find_interface(gchar* interface, GHashTable* netdefs) { GPtrArray *found; GFileInfo *info; GFile *driver_file; gchar *driver_path; gchar *driver = NULL; gpointer key, value; GHashTableIter iter; int ret = EXIT_FAILURE; found = g_ptr_array_new (); /* Try to get the driver name for the interface... */ driver_path = g_strdup_printf("/sys/class/net/%s/device/driver", interface); driver_file = g_file_new_for_path (driver_path); info = g_file_query_info (driver_file, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, 0, NULL, NULL); if (info != NULL) { /* testing for driver matching is done via autopkgtest */ // LCOV_EXCL_START driver = g_path_get_basename (g_file_info_get_symlink_target (info)); g_object_unref (info); // LCOV_EXCL_STOP } g_object_unref (driver_file); g_free (driver_path); g_hash_table_iter_init (&iter, netdefs); while (g_hash_table_iter_next (&iter, &key, &value)) { NetplanNetDefinition *nd = (NetplanNetDefinition *) value; if (!g_strcmp0(nd->set_name, interface)) g_ptr_array_add (found, (gpointer) nd); else if (!g_strcmp0(nd->id, interface)) g_ptr_array_add (found, (gpointer) nd); else if (!g_strcmp0(nd->match.original_name, interface)) g_ptr_array_add (found, (gpointer) nd); } if (found->len == 0 && driver != NULL) { /* testing for driver matching is done via autopkgtest */ // LCOV_EXCL_START g_hash_table_iter_init (&iter, netdefs); while (g_hash_table_iter_next (&iter, &key, &value)) { NetplanNetDefinition *nd = (NetplanNetDefinition *) value; if (!g_strcmp0(nd->match.driver, driver)) g_ptr_array_add (found, (gpointer) nd); } // LCOV_EXCL_STOP } if (driver) g_free (driver); // LCOV_EXCL_LINE if (found->len != 1) { goto exit_find; } else { const NetplanNetDefinition *nd = (NetplanNetDefinition *)g_ptr_array_index (found, 0); g_printf("id=%s, backend=%s, set_name=%s, match_name=%s, match_mac=%s, match_driver=%s\n", nd->id, netplan_backend_name(nd->backend), nd->set_name, nd->match.original_name, nd->match.mac, nd->match.driver); } ret = EXIT_SUCCESS; exit_find: g_ptr_array_free (found, TRUE); return ret; } #define CHECK_CALL(call) {\ if (!call) {\ error_code = 1; \ fprintf(stderr, "%s\n", error->message); \ goto cleanup;\ }\ } int main(int argc, char** argv) { NetplanError* error = NULL; GOptionContext* opt_context; /* are we being called as systemd generator? */ gboolean called_as_generator = (strstr(argv[0], "systemd/system-generators/") != NULL); g_autofree char* generator_run_stamp = NULL; glob_t gl; int error_code = 0; NetplanParser* npp = NULL; NetplanState* np_state = NULL; /* Parse CLI options */ opt_context = g_option_context_new(NULL); if (called_as_generator) g_option_context_set_help_enabled(opt_context, FALSE); g_option_context_set_summary(opt_context, "Generate backend network configuration from netplan YAML definition."); g_option_context_set_description(opt_context, "This program reads the specified netplan YAML definition file(s)\n" "or, if none are given, /etc/netplan/*.yaml.\n" "It then generates the corresponding systemd-networkd, NetworkManager,\n" "and udev configuration files in /run."); g_option_context_add_main_entries(opt_context, options, NULL); if (!g_option_context_parse(opt_context, &argc, &argv, &error)) { fprintf(stderr, "failed to parse options: %s\n", error->message); return 1; } if (called_as_generator) { if (files == NULL || g_strv_length(files) != 3 || files[0] == NULL) { g_fprintf(stderr, "%s can not be called directly, use 'netplan generate'.", argv[0]); return 1; } generator_run_stamp = g_build_path(G_DIR_SEPARATOR_S, files[0], "netplan.stamp", NULL); if (g_access(generator_run_stamp, F_OK) == 0) { g_fprintf(stderr, "netplan generate already ran, remove %s to force re-run\n", generator_run_stamp); return 0; } } npp = netplan_parser_new(); /* Read all input files */ if (files && !called_as_generator) { for (gchar** f = files; f && *f; ++f) { CHECK_CALL(netplan_parser_load_yaml(npp, *f, &error)); } } else CHECK_CALL(netplan_parser_load_yaml_hierarchy(npp, rootdir, &error)); np_state = netplan_state_new(); CHECK_CALL(netplan_state_import_parser_results(np_state, npp, &error)); if (mapping_iface) { if (np_state->netdefs) error_code = find_interface(mapping_iface, np_state->netdefs); else error_code = 1; goto cleanup; } /* Clean up generated config from previous runs */ _netplan_networkd_cleanup(rootdir); _netplan_nm_cleanup(rootdir); _netplan_ovs_cleanup(rootdir); _netplan_sriov_cleanup(rootdir); /* Generate backend specific configuration files from merged data. */ CHECK_CALL(netplan_state_finish_ovs_write(np_state, rootdir, &error)); // OVS cleanup unit is always written if (np_state->netdefs) { g_debug("Generating output files.."); for (GList* iterator = np_state->netdefs_ordered; iterator; iterator = iterator->next) { NetplanNetDefinition* def = (NetplanNetDefinition*) iterator->data; gboolean has_been_written = FALSE; CHECK_CALL(_netplan_netdef_write_networkd(np_state, def, rootdir, &has_been_written, &error)); any_networkd = any_networkd || has_been_written; CHECK_CALL(_netplan_netdef_write_ovs(np_state, def, rootdir, &has_been_written, &error)); CHECK_CALL(_netplan_netdef_write_nm(np_state, def, rootdir, &has_been_written, &error)); any_nm = any_nm || has_been_written; } CHECK_CALL(netplan_state_finish_nm_write(np_state, rootdir, &error)); CHECK_CALL(netplan_state_finish_sriov_write(np_state, rootdir, &error)); /* We may have written .rules & .link files, thus we must * invalidate udevd cache of its config as by default it only * invalidates cache at most every 3 seconds. Not sure if this * should live in `generate' or `apply', but it is confusing * when udevd ignores just-in-time created rules files. */ reload_udevd(); } /* Disable /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf * (which restricts NM to wifi and wwan) if "renderer: NetworkManager" is used anywhere */ if (netplan_state_get_backend(np_state) == NETPLAN_BACKEND_NM || any_nm) _netplan_g_string_free_to_file(g_string_new(NULL), rootdir, "/run/NetworkManager/conf.d/10-globally-managed-devices.conf", NULL); if (called_as_generator) { /* Ensure networkd starts if we have any configuration for it */ if (any_networkd) enable_networkd(files[0]); /* Leave a stamp file so that we don't regenerate the configuration * multiple times and userspace can wait for it to finish */ FILE* f = fopen(generator_run_stamp, "w"); g_assert(f != NULL); fclose(f); } else if (check_called_just_in_time()) { /* netplan-feature: generate-just-in-time */ /* When booting with cloud-init, network configuration * might be provided just-in-time. Specifically after * system-generators were executed, but before * network.target is started. In such case, auxiliary * units that netplan enables have not been included in * the initial boot transaction. Detect such scenario and * add all netplan units to the initial boot transaction. */ // LCOV_EXCL_START /* covered via 'cloud-init' integration test */ if (any_networkd) { start_unit_jit("systemd-networkd.socket"); start_unit_jit("systemd-networkd-wait-online.service"); start_unit_jit("systemd-networkd.service"); } g_autofree char* glob_run = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, "run/systemd/system/netplan-*.service", NULL); if (!glob(glob_run, 0, NULL, &gl)) { for (size_t i = 0; i < gl.gl_pathc; ++i) { gchar *unit_name = g_path_get_basename(gl.gl_pathv[i]); start_unit_jit(unit_name); g_free(unit_name); } } // LCOV_EXCL_STOP } cleanup: g_option_context_free(opt_context); if (error) g_error_free(error); if (npp) netplan_parser_clear(&npp); if (np_state) netplan_state_clear(&np_state); return error_code; } netplan-1.0/src/meson.build000066400000000000000000000020731457004145200157500ustar00rootroot00000000000000sources = files( 'error.c', 'names.c', 'netplan.c', 'networkd.c', 'nm.c', 'openvswitch.c', 'parse.c', 'parse-nm.c', 'sriov.c', 'types.c', 'util.c', 'validation.c') libnetplan = library( 'netplan', sources, gnu_symbol_visibility: 'hidden', dependencies: [glib, gio, yaml, uuid], include_directories: inc, soversion: 1, install: true) libnetplan_testing = library( 'netplan_testing', sources, gnu_symbol_visibility: 'default', c_args: ['-DUNITTESTS'], dependencies: [glib, gio, yaml, uuid], include_directories: inc, soversion: 1, install: false) libexec_netplan = join_paths(get_option('libexecdir'), 'netplan') executable( 'generate', 'generate.c', include_directories: inc, link_with: libnetplan, dependencies: [glib, gio, yaml, uuid], install_dir: libexec_netplan, install: true) meson.add_install_script(meson_make_symlink, join_paths(get_option('prefix'), libexec_netplan, 'generate'), join_paths(systemd_generator_dir, 'netplan')) netplan-1.0/src/names.c000066400000000000000000000132511457004145200150550ustar00rootroot00000000000000/* * Copyright (C) 2021 Canonical, Ltd. * Author: Simon Chopin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "names.h" #include "parse.h" /* Non-static as we need it for ABI compatibility, see at the end of the file */ const char* const netplan_backend_to_str[NETPLAN_BACKEND_MAX_] = { [NETPLAN_BACKEND_NONE] = "none", [NETPLAN_BACKEND_NETWORKD] = "networkd", [NETPLAN_BACKEND_NM] = "NetworkManager", [NETPLAN_BACKEND_OVS] = "OpenVSwitch", }; static const char* const netplan_wifi_mode_to_str[NETPLAN_WIFI_MODE_MAX_] = { [NETPLAN_WIFI_MODE_INFRASTRUCTURE] = "infrastructure", [NETPLAN_WIFI_MODE_ADHOC] = "adhoc", [NETPLAN_WIFI_MODE_AP] = "ap", [NETPLAN_WIFI_MODE_OTHER] = NULL, }; static const char* const netplan_def_type_to_str[NETPLAN_DEF_TYPE_MAX_] = { [NETPLAN_DEF_TYPE_NONE] = NULL, [NETPLAN_DEF_TYPE_ETHERNET] = "ethernets", [NETPLAN_DEF_TYPE_WIFI] = "wifis", [NETPLAN_DEF_TYPE_MODEM] = "modems", [NETPLAN_DEF_TYPE_BRIDGE] = "bridges", [NETPLAN_DEF_TYPE_BOND] = "bonds", [NETPLAN_DEF_TYPE_VLAN] = "vlans", [NETPLAN_DEF_TYPE_VRF] = "vrfs", [NETPLAN_DEF_TYPE_TUNNEL] = "tunnels", [NETPLAN_DEF_TYPE_DUMMY] = "dummy-devices", /* wokeignore:rule=dummy */ [NETPLAN_DEF_TYPE_VETH] = "virtual-ethernets", [NETPLAN_DEF_TYPE_PORT] = "_ovs-ports", [NETPLAN_DEF_TYPE_NM] = "nm-devices", }; static const char* const netplan_auth_key_management_type_to_str[NETPLAN_AUTH_KEY_MANAGEMENT_MAX] = { [NETPLAN_AUTH_KEY_MANAGEMENT_NONE] = "none", [NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK] = "psk", [NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP] = "eap", [NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSHA256] = "eap-sha256", [NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSUITE_B_192] = "eap-suite-b-192", [NETPLAN_AUTH_KEY_MANAGEMENT_WPA_SAE] = "sae", [NETPLAN_AUTH_KEY_MANAGEMENT_8021X] = "802.1x", }; static const char* const netplan_auth_eap_method_to_str[NETPLAN_AUTH_EAP_METHOD_MAX] = { [NETPLAN_AUTH_EAP_NONE] = NULL, [NETPLAN_AUTH_EAP_TLS] = "tls", [NETPLAN_AUTH_EAP_PEAP] = "peap", [NETPLAN_AUTH_EAP_TTLS] = "ttls", [NETPLAN_AUTH_EAP_LEAP] = "leap", [NETPLAN_AUTH_EAP_PWD] = "pwd", }; static const char* const netplan_tunnel_mode_to_str[NETPLAN_TUNNEL_MODE_MAX_] = { [NETPLAN_TUNNEL_MODE_UNKNOWN] = NULL, [NETPLAN_TUNNEL_MODE_IPIP] = "ipip", [NETPLAN_TUNNEL_MODE_GRE] = "gre", [NETPLAN_TUNNEL_MODE_SIT] = "sit", [NETPLAN_TUNNEL_MODE_ISATAP] = "isatap", [NETPLAN_TUNNEL_MODE_VTI] = "vti", [NETPLAN_TUNNEL_MODE_IP6IP6] = "ip6ip6", [NETPLAN_TUNNEL_MODE_IPIP6] = "ipip6", [NETPLAN_TUNNEL_MODE_IP6GRE] = "ip6gre", [NETPLAN_TUNNEL_MODE_VTI6] = "vti6", [NETPLAN_TUNNEL_MODE_GRETAP] = "gretap", [NETPLAN_TUNNEL_MODE_IP6GRETAP] = "ip6gretap", [NETPLAN_TUNNEL_MODE_VXLAN] = "vxlan", [NETPLAN_TUNNEL_MODE_WIREGUARD] = "wireguard", }; static const char* const netplan_addr_gen_mode_to_str[NETPLAN_ADDRGEN_MAX] = { [NETPLAN_ADDRGEN_DEFAULT] = NULL, [NETPLAN_ADDRGEN_EUI64] = "eui64", [NETPLAN_ADDRGEN_STABLEPRIVACY] = "stable-privacy" }; static const char* const netplan_infiniband_mode_to_str[NETPLAN_IB_MODE_MAX_] = { [NETPLAN_IB_MODE_KERNEL] = NULL, [NETPLAN_IB_MODE_DATAGRAM] = "datagram", [NETPLAN_IB_MODE_CONNECTED] = "connected" }; static const char* const netplan_key_flags_to_str[NETPLAN_KEY_FLAG_MAX_] = { [NETPLAN_KEY_FLAG_NONE] = NULL, [NETPLAN_KEY_FLAG_AGENT_OWNED] = "agent-owned", [NETPLAN_KEY_FLAG_NOT_SAVED] = "not-saved", [NETPLAN_KEY_FLAG_NOT_REQUIRED] = "not-required", }; #define NAME_FUNCTION(_radical, _type) const char *netplan_ ## _radical ## _name( _type val) \ { \ return (val < sizeof(netplan_ ## _radical ## _to_str) / sizeof(char *)) ? netplan_ ## _radical ## _to_str [val] : NULL; \ } /* @num_flags needs to account for the 0x0 value (index 0), which doesn't * represent any flag, but still exists. So subtract 1 from the array length. */ #define NAME_FUNCTION_FLAGS(_radical) const char *netplan_ ## _radical ## _name( NetplanFlags val) \ { \ size_t num_flags = sizeof(netplan_ ## _radical ## _to_str) / sizeof(char *) - 1; \ return (val <= 1 << (num_flags - 1)) ? netplan_ ## _radical ## _to_str [__builtin_ffs(val)] : NULL; \ } NAME_FUNCTION(backend, NetplanBackend); NAME_FUNCTION(def_type, NetplanDefType); NAME_FUNCTION(auth_key_management_type, NetplanAuthKeyManagementType); NAME_FUNCTION(auth_eap_method, NetplanAuthEAPMethod); NAME_FUNCTION(tunnel_mode, NetplanTunnelMode); NAME_FUNCTION(addr_gen_mode, NetplanAddrGenMode); NAME_FUNCTION(wifi_mode, NetplanWifiMode); NAME_FUNCTION(infiniband_mode, NetplanInfinibandMode); NAME_FUNCTION(key_flags, NetplanKeyFlags); NAME_FUNCTION_FLAGS(vxlan_notification); NAME_FUNCTION_FLAGS(vxlan_checksum); NAME_FUNCTION_FLAGS(vxlan_extension); #define ENUM_FUNCTION(_radical, _type) _type netplan_ ## _radical ## _from_name(const char* val) \ { \ for (size_t i = 0; i < sizeof(netplan_ ## _radical ## _to_str); ++i) { \ if (g_strcmp0(val, netplan_ ## _radical ## _to_str[i]) == 0) \ return i; \ } \ return NETPLAN_DEF_TYPE_NONE; \ } ENUM_FUNCTION(def_type, NetplanDefType); netplan-1.0/src/names.h000066400000000000000000000046171457004145200150700ustar00rootroot00000000000000/* * Copyright (C) 2021 Canonical, Ltd. * Author: Simon Chopin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "netplan.h" #include "types-internal.h" NETPLAN_INTERNAL const char* netplan_backend_name(NetplanBackend val); NETPLAN_INTERNAL const char* netplan_def_type_name(NetplanDefType val); const char* netplan_auth_key_management_type_name(NetplanAuthKeyManagementType val); const char* netplan_auth_eap_method_name(NetplanAuthEAPMethod val); const char* netplan_tunnel_mode_name(NetplanTunnelMode val); const char* netplan_addr_gen_mode_name(NetplanAddrGenMode val); const char* netplan_wifi_mode_name(NetplanWifiMode val); const char* netplan_infiniband_mode_name(NetplanInfinibandMode val); const char* netplan_key_flags_name(NetplanKeyFlags val); const char* netplan_vxlan_notification_name(int val); const char* netplan_vxlan_checksum_name(int val); const char* netplan_vxlan_extension_name(int val); NetplanDefType netplan_def_type_from_name(const char* val); /* Netplan flag names */ static const char* const netplan_vxlan_notification_to_str[] = { [__builtin_ffs(NETPLAN_VXLAN_NOTIFICATION_L2_MISS)] = "l2-miss", [__builtin_ffs(NETPLAN_VXLAN_NOTIFICATION_L3_MISS)] = "l3-miss", }; static const char* const netplan_vxlan_checksum_to_str[] = { [__builtin_ffs(NETPLAN_VXLAN_CHECKSUM_UDP)] = "udp", [__builtin_ffs(NETPLAN_VXLAN_CHECKSUM_ZERO_UDP6_TX)] = "zero-udp6-tx", [__builtin_ffs(NETPLAN_VXLAN_CHECKSUM_ZERO_UDP6_RX)] = "zero-udp6-rx", [__builtin_ffs(NETPLAN_VXLAN_CHECKSUM_REMOTE_TX)] = "remote-tx", [__builtin_ffs(NETPLAN_VXLAN_CHECKSUM_REMOTE_RX)] = "remote-rx", }; static const char* const netplan_vxlan_extension_to_str[] = { [__builtin_ffs(NETPLAN_VXLAN_EXTENSION_GROUP_POLICY)] = "group-policy", [__builtin_ffs(NETPLAN_VXLAN_EXTENSION_GENERIC_PROTOCOL)] = "generic-protocol", }; netplan-1.0/src/netplan.c000066400000000000000000001577311457004145200154270ustar00rootroot00000000000000/* * Copyright (C) 2021-2023 Canonical, Ltd. * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include "netplan.h" #include "parse.h" #include "types-internal.h" #include "yaml-helpers.h" #include "util-internal.h" #include "names.h" gchar *tmp = NULL; #define DIRTY(_def, _data) \ ((_def)->_private && \ (_def)->_private->dirty_fields && \ g_hash_table_contains((_def)->_private->dirty_fields, &(_data))) #define DIRTY_REF(_def, _data_ref) \ ((_def)->_private && \ (_def)->_private->dirty_fields && \ g_hash_table_contains((_def)->_private->dirty_fields, _data_ref)) #define YAML_STRING(_def, event_ptr, emitter_ptr, key, value_ptr) {\ if (value_ptr) { \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ YAML_SCALAR_QUOTED(event_ptr, emitter_ptr, value_ptr); \ } else if DIRTY(_def, value_ptr) { \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ YAML_NULL_PLAIN(event_ptr, emitter_ptr); \ } \ }\ #define YAML_STRING_PLAIN(_def, event_ptr, emitter_ptr, key, value_ptr) {\ if (value_ptr) { \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, value_ptr); \ } else if DIRTY(_def, value_ptr) { \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ YAML_NULL_PLAIN(event_ptr, emitter_ptr); \ } \ }\ #define YAML_UINT_DEFAULT(_def, event_ptr, emitter_ptr, key, value, default_value) {\ if (value != default_value) { \ _YAML_UINT(event_ptr, emitter_ptr, key, value); \ } else if DIRTY(_def, value) { \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ YAML_NULL_PLAIN(event_ptr, emitter_ptr); \ } \ }\ #define YAML_UINT_0(_def, event_ptr, emitter_ptr, key, value) \ YAML_UINT_DEFAULT(_def, event_ptr, emitter_ptr, key, value, 0); #define YAML_BOOL_TRUE(_def, event_ptr, emitter_ptr, key, value) {\ if (value) { \ YAML_NONNULL_STRING_PLAIN(event_ptr, emitter_ptr, key, "true"); \ } else if DIRTY(_def, value) { \ YAML_NONNULL_STRING_PLAIN(event_ptr, emitter_ptr, key, "false"); \ } \ }\ #define YAML_BOOL_FALSE(_def, event_ptr, emitter_ptr, key, value) {\ if (!value) { \ YAML_NONNULL_STRING_PLAIN(event_ptr, emitter_ptr, key, "false"); \ } else if DIRTY(_def, value) { \ YAML_NONNULL_STRING_PLAIN(event_ptr, emitter_ptr, key, "true"); \ } \ }\ #define YAML_BOOL_TRISTATE(_def, event_ptr, emitter_ptr, key, value) {\ if (value == NETPLAN_TRISTATE_TRUE) { \ YAML_NONNULL_STRING_PLAIN(event_ptr, emitter_ptr, key, "true"); \ } else if (value == NETPLAN_TRISTATE_FALSE) { \ YAML_NONNULL_STRING_PLAIN(event_ptr, emitter_ptr, key, "false"); \ } \ } #define DIRTY_COMPLEX(_def, _data) complex_object_is_dirty(_def, (char*)(&_data), sizeof(_data)) STATIC gboolean write_match(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { YAML_SCALAR_PLAIN(event, emitter, "match"); YAML_MAPPING_OPEN(event, emitter); YAML_NONNULL_STRING(event, emitter, "name", def->match.original_name); YAML_NONNULL_STRING(event, emitter, "macaddress", def->match.mac) if (def->match.driver && strchr(def->match.driver, '\t')) { gchar **split = g_strsplit(def->match.driver, "\t", 0); YAML_SCALAR_PLAIN(event, emitter, "driver"); YAML_SEQUENCE_OPEN(event, emitter); for (unsigned i = 0; split[i]; ++i) YAML_SCALAR_QUOTED(event, emitter, split[i]); YAML_SEQUENCE_CLOSE(event, emitter); g_strfreev(split); } else YAML_NONNULL_STRING(event, emitter, "driver", def->match.driver); YAML_MAPPING_CLOSE(event, emitter); return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_auth(yaml_event_t* event, yaml_emitter_t* emitter, NetplanAuthenticationSettings auth) { YAML_SCALAR_PLAIN(event, emitter, "auth"); YAML_MAPPING_OPEN(event, emitter); YAML_NONNULL_STRING(event, emitter, "key-management", netplan_auth_key_management_type_name(auth.key_management)); YAML_NONNULL_STRING(event, emitter, "method", netplan_auth_eap_method_name(auth.eap_method)); YAML_NONNULL_STRING(event, emitter, "anonymous-identity", auth.anonymous_identity); YAML_NONNULL_STRING(event, emitter, "identity", auth.identity); YAML_NONNULL_STRING(event, emitter, "ca-certificate", auth.ca_certificate); YAML_NONNULL_STRING(event, emitter, "client-certificate", auth.client_certificate); YAML_NONNULL_STRING(event, emitter, "client-key", auth.client_key); YAML_NONNULL_STRING(event, emitter, "client-key-password", auth.client_key_password); YAML_NONNULL_STRING(event, emitter, "phase2-auth", auth.phase2_auth); if (!auth.password && auth.psk) { YAML_NONNULL_STRING(event, emitter, "password", auth.psk); } else { YAML_NONNULL_STRING(event, emitter, "password", auth.password); } YAML_MAPPING_CLOSE(event, emitter); return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_bond_params(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { if (DIRTY(def, def->bond_params) || def->bond_params.mode || def->bond_params.monitor_interval || def->bond_params.up_delay || def->bond_params.down_delay || def->bond_params.lacp_rate || def->bond_params.transmit_hash_policy || def->bond_params.selection_logic || def->bond_params.arp_validate || def->bond_params.arp_all_targets || def->bond_params.fail_over_mac_policy || def->bond_params.primary_reselect_policy || def->bond_params.learn_interval || def->bond_params.arp_interval || def->bond_params.primary_member || def->bond_params.min_links || def->bond_params.all_members_active || def->bond_params.gratuitous_arp || def->bond_params.packets_per_member || def->bond_params.resend_igmp || def->bond_params.arp_ip_targets) { YAML_SCALAR_PLAIN(event, emitter, "parameters"); YAML_MAPPING_OPEN(event, emitter); YAML_STRING(def, event, emitter, "mode", def->bond_params.mode); YAML_STRING(def, event, emitter, "mii-monitor-interval", def->bond_params.monitor_interval); YAML_STRING(def, event, emitter, "up-delay", def->bond_params.up_delay); YAML_STRING(def, event, emitter, "down-delay", def->bond_params.down_delay); YAML_STRING(def, event, emitter, "lacp-rate", def->bond_params.lacp_rate); YAML_STRING(def, event, emitter, "transmit-hash-policy", def->bond_params.transmit_hash_policy); YAML_STRING(def, event, emitter, "ad-select", def->bond_params.selection_logic); YAML_STRING(def, event, emitter, "arp-validate", def->bond_params.arp_validate); YAML_STRING(def, event, emitter, "arp-all-targets", def->bond_params.arp_all_targets); YAML_STRING(def, event, emitter, "fail-over-mac-policy", def->bond_params.fail_over_mac_policy); YAML_STRING(def, event, emitter, "primary-reselect-policy", def->bond_params.primary_reselect_policy); YAML_STRING(def, event, emitter, "learn-packet-interval", def->bond_params.learn_interval); YAML_STRING(def, event, emitter, "arp-interval", def->bond_params.arp_interval); YAML_STRING(def, event, emitter, "primary", def->bond_params.primary_member); YAML_UINT_0(def, event, emitter, "min-links", def->bond_params.min_links); YAML_BOOL_TRUE(def, event, emitter, "all-members-active", def->bond_params.all_members_active); YAML_UINT_0(def, event, emitter, "gratuitous-arp", def->bond_params.gratuitous_arp); YAML_UINT_0(def, event, emitter, "packets-per-member", def->bond_params.packets_per_member); YAML_UINT_0(def, event, emitter, "resend-igmp", def->bond_params.resend_igmp); if (def->bond_params.arp_ip_targets || DIRTY(def, def->bond_params.arp_ip_targets)) { GArray* arr = def->bond_params.arp_ip_targets; YAML_SCALAR_PLAIN(event, emitter, "arp-ip-targets"); YAML_SEQUENCE_OPEN(event, emitter); if (arr) for (unsigned i = 0; i < arr->len; ++i) YAML_SCALAR_PLAIN(event, emitter, g_array_index(arr, char*, i)); YAML_SEQUENCE_CLOSE(event, emitter); } YAML_MAPPING_CLOSE(event, emitter); } return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_vxlan(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { if (def->type == NETPLAN_DEF_TYPE_TUNNEL && def->tunnel.mode == NETPLAN_TUNNEL_MODE_VXLAN) { g_assert(def->vxlan); YAML_UINT_0(def, event, emitter, "id", def->vxlan->vni); if (def->vxlan->link) YAML_STRING(def, event, emitter, "link", def->vxlan->link->id); if (def->vxlan->source_port_min && def->vxlan->source_port_max) { YAML_SCALAR_PLAIN(event, emitter, "port-range"); YAML_SEQUENCE_OPEN(event, emitter); tmp = g_strdup_printf("%u", def->vxlan->source_port_min); YAML_SCALAR_PLAIN(event, emitter, tmp); g_free(tmp); tmp = g_strdup_printf("%u", def->vxlan->source_port_max); YAML_SCALAR_PLAIN(event, emitter, tmp); g_free(tmp); YAML_SEQUENCE_CLOSE(event, emitter); } YAML_UINT_DEFAULT(def, event, emitter, "flow-label", def->vxlan->flow_label, G_MAXUINT); YAML_UINT_0(def, event, emitter, "limit", def->vxlan->limit); YAML_UINT_0(def, event, emitter, "type-of-service", def->vxlan->tos); YAML_UINT_0(def, event, emitter, "ageing", def->vxlan->ageing); YAML_BOOL_TRISTATE(def, event, emitter, "mac-learning", def->vxlan->mac_learning); YAML_BOOL_TRISTATE(def, event, emitter, "arp-proxy", def->vxlan->arp_proxy); YAML_BOOL_TRISTATE(def, event, emitter, "short-circuit", def->vxlan->short_circuit); YAML_BOOL_TRISTATE(def, event, emitter, "do-not-fragment", def->vxlan->do_not_fragment); if (def->vxlan->notifications) { YAML_SCALAR_PLAIN(event, emitter, "notifications"); YAML_SEQUENCE_OPEN(event, emitter); int f = def->vxlan->notifications; const char* (*fn)(int) = &netplan_vxlan_notification_name; YAML_FLAG(event, emitter, NETPLAN_VXLAN_NOTIFICATION_L2_MISS, f, (*fn)); YAML_FLAG(event, emitter, NETPLAN_VXLAN_NOTIFICATION_L3_MISS, f, (*fn)); YAML_SEQUENCE_CLOSE(event, emitter); } if (def->vxlan->checksums) { YAML_SCALAR_PLAIN(event, emitter, "checksums"); YAML_SEQUENCE_OPEN(event, emitter); int f = def->vxlan->checksums; const char* (*fn)(int) = &netplan_vxlan_checksum_name; YAML_FLAG(event, emitter, NETPLAN_VXLAN_CHECKSUM_UDP, f, (*fn)); YAML_FLAG(event, emitter, NETPLAN_VXLAN_CHECKSUM_ZERO_UDP6_TX, f, (*fn)); YAML_FLAG(event, emitter, NETPLAN_VXLAN_CHECKSUM_ZERO_UDP6_RX, f, (*fn)); YAML_FLAG(event, emitter, NETPLAN_VXLAN_CHECKSUM_REMOTE_TX, f, (*fn)); YAML_FLAG(event, emitter, NETPLAN_VXLAN_CHECKSUM_REMOTE_RX, f, (*fn)); YAML_SEQUENCE_CLOSE(event, emitter); } if (def->vxlan->extensions) { YAML_SCALAR_PLAIN(event, emitter, "extensions"); YAML_SEQUENCE_OPEN(event, emitter); int f = def->vxlan->extensions; const char* (*fn)(int) = &netplan_vxlan_extension_name; YAML_FLAG(event, emitter, NETPLAN_VXLAN_EXTENSION_GROUP_POLICY, f, (*fn)); YAML_FLAG(event, emitter, NETPLAN_VXLAN_EXTENSION_GENERIC_PROTOCOL, f, (*fn)); YAML_SEQUENCE_CLOSE(event, emitter); } } return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_bridge_params(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def, const GArray *interfaces) { if (def->custom_bridging || DIRTY_COMPLEX(def, def->bridge_params)) { gboolean has_path_cost = FALSE; gboolean has_port_priority = FALSE; for (unsigned i = 0; i < interfaces->len; ++i) { NetplanNetDefinition *nd = g_array_index(interfaces, NetplanNetDefinition*, i); has_path_cost = has_path_cost || !!nd->bridge_params.path_cost; has_port_priority = has_port_priority || !!nd->bridge_params.port_priority; if (has_path_cost && has_port_priority) break; /* no need to continue this check */ } YAML_SCALAR_PLAIN(event, emitter, "parameters"); YAML_MAPPING_OPEN(event, emitter); YAML_STRING(def, event, emitter, "ageing-time", def->bridge_params.ageing_time); YAML_STRING(def, event, emitter, "forward-delay", def->bridge_params.forward_delay); YAML_STRING(def, event, emitter, "hello-time", def->bridge_params.hello_time); YAML_STRING(def, event, emitter, "max-age", def->bridge_params.max_age); YAML_UINT_0(def, event, emitter, "priority", def->bridge_params.priority); YAML_BOOL_FALSE(def, event, emitter, "stp", def->bridge_params.stp); if (has_port_priority) { YAML_SCALAR_PLAIN(event, emitter, "port-priority"); YAML_MAPPING_OPEN(event, emitter); for (unsigned i = 0; i < interfaces->len; ++i) { NetplanNetDefinition *nd = g_array_index(interfaces, NetplanNetDefinition*, i); YAML_UINT_0(nd, event, emitter, nd->id, nd->bridge_params.port_priority); } YAML_MAPPING_CLOSE(event, emitter); } if (has_path_cost) { YAML_SCALAR_PLAIN(event, emitter, "path-cost"); YAML_MAPPING_OPEN(event, emitter); for (unsigned i = 0; i < interfaces->len; ++i) { NetplanNetDefinition *nd = g_array_index(interfaces, NetplanNetDefinition*, i); YAML_UINT_0(nd, event, emitter, nd->id, nd->bridge_params.path_cost); } YAML_MAPPING_CLOSE(event, emitter); } YAML_MAPPING_CLOSE(event, emitter); } return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_modem_params(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { /* some modem settings to auto-detect GSM vs CDMA connections */ YAML_BOOL_TRUE(def, event, emitter, "auto-config", def->modem_params.auto_config); YAML_NONNULL_STRING(event, emitter, "apn", def->modem_params.apn); YAML_NONNULL_STRING(event, emitter, "device-id", def->modem_params.device_id); YAML_NONNULL_STRING(event, emitter, "network-id", def->modem_params.network_id); YAML_NONNULL_STRING(event, emitter, "pin", def->modem_params.pin); YAML_NONNULL_STRING(event, emitter, "sim-id", def->modem_params.sim_id); YAML_NONNULL_STRING(event, emitter, "sim-operator-id", def->modem_params.sim_operator_id); YAML_NONNULL_STRING(event, emitter, "username", def->modem_params.username); YAML_NONNULL_STRING(event, emitter, "password", def->modem_params.password); YAML_NONNULL_STRING(event, emitter, "number", def->modem_params.number); return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } typedef struct { yaml_event_t* event; yaml_emitter_t* emitter; } _passthrough_handler_data; STATIC void _passthrough_handler(GQuark key_id, gpointer value, gpointer user_data) { _passthrough_handler_data *d = user_data; const gchar* key = g_quark_to_string(key_id); YAML_NONNULL_STRING(d->event, d->emitter, key, value); err_path: return; // LCOV_EXCL_LINE } STATIC gboolean write_backend_settings(yaml_event_t* event, yaml_emitter_t* emitter, NetplanBackendSettings s) { if (s.uuid || s.name || s.passthrough) { YAML_SCALAR_PLAIN(event, emitter, "networkmanager"); YAML_MAPPING_OPEN(event, emitter); YAML_NONNULL_STRING(event, emitter, "uuid", s.uuid); YAML_NONNULL_STRING(event, emitter, "name", s.name); if (s.passthrough) { YAML_SCALAR_PLAIN(event, emitter, "passthrough"); YAML_MAPPING_OPEN(event, emitter); _passthrough_handler_data d; d.event = event; d.emitter = emitter; g_datalist_foreach(&s.passthrough, _passthrough_handler, &d); YAML_MAPPING_CLOSE(event, emitter); } YAML_MAPPING_CLOSE(event, emitter); } return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_access_points(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { NetplanWifiAccessPoint* ap = NULL; GHashTableIter iter; gpointer key, value; YAML_SCALAR_PLAIN(event, emitter, "access-points"); YAML_MAPPING_OPEN(event, emitter); g_hash_table_iter_init(&iter, def->access_points); while (g_hash_table_iter_next(&iter, &key, &value)) { ap = value; YAML_SCALAR_QUOTED(event, emitter, ap->ssid); YAML_MAPPING_OPEN(event, emitter); YAML_BOOL_TRUE(def, event, emitter, "hidden", ap->hidden); YAML_STRING(def, event, emitter, "bssid", ap->bssid); if (ap->band == NETPLAN_WIFI_BAND_5) { YAML_NONNULL_STRING(event, emitter, "band", "5GHz"); } else if (ap->band == NETPLAN_WIFI_BAND_24) { YAML_NONNULL_STRING(event, emitter, "band", "2.4GHz"); } YAML_UINT_0(def, event, emitter, "channel", ap->channel); if (ap->auth.psk && !_is_auth_key_management_psk(&ap->auth)) YAML_NONNULL_STRING(event, emitter, "password", ap->auth.psk); if (ap->has_auth || DIRTY(def, ap->auth)) write_auth(event, emitter, ap->auth); if (ap->mode != NETPLAN_WIFI_MODE_INFRASTRUCTURE || DIRTY(def, ap->mode)) YAML_NONNULL_STRING(event, emitter, "mode", netplan_wifi_mode_name(ap->mode)); if (!write_backend_settings(event, emitter, ap->backend_settings)) goto err_path; YAML_MAPPING_CLOSE(event, emitter); } YAML_MAPPING_CLOSE(event, emitter); return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_addresses(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { YAML_SCALAR_PLAIN(event, emitter, "addresses"); YAML_SEQUENCE_OPEN(event, emitter); if (def->address_options) { for (unsigned i = 0; i < def->address_options->len; ++i) { NetplanAddressOptions *opts = g_array_index(def->address_options, NetplanAddressOptions*, i); YAML_MAPPING_OPEN(event, emitter); YAML_SCALAR_QUOTED(event, emitter, opts->address); YAML_MAPPING_OPEN(event, emitter); YAML_NONNULL_STRING(event, emitter, "label", opts->label); YAML_NONNULL_STRING(event, emitter, "lifetime", opts->lifetime); YAML_MAPPING_CLOSE(event, emitter); YAML_MAPPING_CLOSE(event, emitter); } } if (def->ip4_addresses) { for (unsigned i = 0; i < def->ip4_addresses->len; ++i) YAML_SCALAR_QUOTED(event, emitter, g_array_index(def->ip4_addresses, char*, i)); } if (def->ip6_addresses) { for (unsigned i = 0; i < def->ip6_addresses->len; ++i) YAML_SCALAR_QUOTED(event, emitter, g_array_index(def->ip6_addresses, char*, i)); } YAML_SEQUENCE_CLOSE(event, emitter); return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_nameservers(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { YAML_SCALAR_PLAIN(event, emitter, "nameservers"); YAML_MAPPING_OPEN(event, emitter); if (def->ip4_nameservers || def->ip6_nameservers){ YAML_SCALAR_PLAIN(event, emitter, "addresses"); YAML_SEQUENCE_OPEN(event, emitter); if (def->ip4_nameservers) { for (unsigned i = 0; i < def->ip4_nameservers->len; ++i) YAML_SCALAR_PLAIN(event, emitter, g_array_index(def->ip4_nameservers, char*, i)); } if (def->ip6_nameservers) { for (unsigned i = 0; i < def->ip6_nameservers->len; ++i) YAML_SCALAR_PLAIN(event, emitter, g_array_index(def->ip6_nameservers, char*, i)); } YAML_SEQUENCE_CLOSE(event, emitter); } if (def->search_domains || DIRTY(def, def->search_domains)){ YAML_SCALAR_PLAIN(event, emitter, "search"); YAML_SEQUENCE_OPEN(event, emitter); if (def->search_domains) { for (unsigned i = 0; i < def->search_domains->len; ++i) YAML_SCALAR_PLAIN(event, emitter, g_array_index(def->search_domains, char*, i)); } YAML_SEQUENCE_CLOSE(event, emitter); } YAML_MAPPING_CLOSE(event, emitter); return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_dhcp_overrides(yaml_event_t* event, yaml_emitter_t* emitter, const char* key, const NetplanNetDefinition* def, const NetplanDHCPOverrides* data) { if (DIRTY_COMPLEX(def, *data) || !data->use_dns || !data->use_ntp || !data->send_hostname || !data->use_hostname || !data->use_mtu || !data->use_routes || data->use_domains || data->hostname || data->metric != NETPLAN_METRIC_UNSPEC) { YAML_SCALAR_PLAIN(event, emitter, key); YAML_MAPPING_OPEN(event, emitter); YAML_BOOL_FALSE(def, event, emitter, "use-dns", data->use_dns); YAML_BOOL_FALSE(def, event, emitter, "use-ntp", data->use_ntp); YAML_BOOL_FALSE(def, event, emitter, "send-hostname", data->send_hostname); YAML_BOOL_FALSE(def, event, emitter, "use-hostname", data->use_hostname); YAML_BOOL_FALSE(def, event, emitter, "use-mtu", data->use_mtu); YAML_BOOL_FALSE(def, event, emitter, "use-routes", data->use_routes); YAML_STRING_PLAIN(def, event, emitter, "use-domains", data->use_domains); YAML_STRING(def, event, emitter, "hostname", data->hostname); YAML_UINT_DEFAULT(def, event, emitter, "route-metric", data->metric, NETPLAN_METRIC_UNSPEC); YAML_MAPPING_CLOSE(event, emitter); } return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_tunnel_settings(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { YAML_NONNULL_STRING(event, emitter, "mode", netplan_tunnel_mode_name(def->tunnel.mode)); YAML_STRING(def, event, emitter, "local", def->tunnel.local_ip); YAML_STRING(def, event, emitter, "remote", def->tunnel.remote_ip); YAML_UINT_0(def, event, emitter, "mark", def->tunnel.fwmark); YAML_UINT_0(def, event, emitter, "port", def->tunnel.port); YAML_UINT_0(def, event, emitter, "ttl", def->tunnel_ttl); /* VXLAN settings */ write_vxlan(event, emitter, def); if (def->tunnel.input_key || def->tunnel.output_key || def->tunnel.private_key || def->tunnel_private_key_flags) { if ( g_strcmp0(def->tunnel.input_key, def->tunnel.output_key) == 0 && g_strcmp0(def->tunnel.input_key, def->tunnel.private_key) == 0 && def->tunnel_private_key_flags == 0) { /* use short form if all keys are the same */ YAML_STRING(def, event, emitter, "key", def->tunnel.input_key); } else { YAML_SCALAR_PLAIN(event, emitter, "keys"); YAML_MAPPING_OPEN(event, emitter); YAML_STRING(def, event, emitter, "input", def->tunnel.input_key); YAML_STRING(def, event, emitter, "output", def->tunnel.output_key); YAML_STRING(def, event, emitter, "private", def->tunnel.private_key); if (def->tunnel_private_key_flags > 0) { YAML_SCALAR_PLAIN(event, emitter, "private-key-flags"); YAML_SEQUENCE_OPEN(event, emitter); for(int i = 1; i < NETPLAN_KEY_FLAG_MAX_; i <<= 1) { if (def->tunnel_private_key_flags & i) YAML_SCALAR_PLAIN(event, emitter, netplan_key_flags_name(i)); } YAML_SEQUENCE_CLOSE(event, emitter); } YAML_MAPPING_CLOSE(event, emitter); } } /* Wireguard peers */ if (def->wireguard_peers && def->wireguard_peers->len > 0) { YAML_SCALAR_PLAIN(event, emitter, "peers"); YAML_SEQUENCE_OPEN(event, emitter); for (unsigned i = 0; i < def->wireguard_peers->len; ++i) { NetplanWireguardPeer *peer = g_array_index(def->wireguard_peers, NetplanWireguardPeer*, i); YAML_MAPPING_OPEN(event, emitter); YAML_STRING(def, event, emitter, "endpoint", peer->endpoint); YAML_UINT_0(def, event, emitter, "keepalive", peer->keepalive); if (peer->public_key || peer->preshared_key) { YAML_SCALAR_PLAIN(event, emitter, "keys"); YAML_MAPPING_OPEN(event, emitter); YAML_STRING(def, event, emitter, "public", peer->public_key); YAML_STRING(def, event, emitter, "shared", peer->preshared_key); YAML_MAPPING_CLOSE(event, emitter); } if (peer->allowed_ips && peer->allowed_ips->len > 0) { YAML_SCALAR_PLAIN(event, emitter, "allowed-ips"); YAML_SEQUENCE_OPEN(event, emitter); for (unsigned i = 0; i < peer->allowed_ips->len; ++i) { char *ip = g_array_index(peer->allowed_ips, char*, i); YAML_SCALAR_QUOTED(event, emitter, ip); } YAML_SEQUENCE_CLOSE(event, emitter); } YAML_MAPPING_CLOSE(event, emitter); } YAML_SEQUENCE_CLOSE(event, emitter); } return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_routes(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { if (def->routes && def->routes->len > 0) { YAML_SCALAR_PLAIN(event, emitter, "routes"); YAML_SEQUENCE_OPEN(event, emitter); for (unsigned i = 0; i < def->routes->len; ++i) { YAML_MAPPING_OPEN(event, emitter); NetplanIPRoute *r = g_array_index(def->routes, NetplanIPRoute*, i); if (r->type && g_strcmp0(r->type, "unicast") != 0) YAML_NONNULL_STRING(event, emitter, "type", r->type); if (r->scope && g_strcmp0(r->scope, "global") != 0) YAML_NONNULL_STRING(event, emitter, "scope", r->scope); YAML_UINT_DEFAULT(def, event, emitter, "metric", r->metric, NETPLAN_METRIC_UNSPEC); /* VRF devices use the VRF routing table implicitly */ if (def->type != NETPLAN_DEF_TYPE_VRF) YAML_UINT_DEFAULT(def, event, emitter, "table", r->table, NETPLAN_ROUTE_TABLE_UNSPEC); YAML_UINT_0(def, event, emitter, "mtu", r->mtubytes); YAML_UINT_0(def, event, emitter, "congestion-window", r->congestion_window); YAML_UINT_0(def, event, emitter, "advertised-receive-window", r->advertised_receive_window); YAML_BOOL_TRUE(def, event, emitter, "on-link", r->onlink); YAML_STRING(def, event, emitter, "from", r->from); YAML_STRING(def, event, emitter, "to", r->to); YAML_STRING(def, event, emitter, "via", r->via); YAML_MAPPING_CLOSE(event, emitter); } YAML_SEQUENCE_CLOSE(event, emitter); } if (def->ip_rules && def->ip_rules->len > 0) { YAML_SCALAR_PLAIN(event, emitter, "routing-policy"); YAML_SEQUENCE_OPEN(event, emitter); for (unsigned i = 0; i < def->ip_rules->len; ++i) { NetplanIPRule *r = g_array_index(def->ip_rules, NetplanIPRule*, i); YAML_MAPPING_OPEN(event, emitter); /* VRF devices use the VRF routing table implicitly */ if (def->type != NETPLAN_DEF_TYPE_VRF) YAML_UINT_DEFAULT(def, event, emitter, "table", r->table, NETPLAN_ROUTE_TABLE_UNSPEC); YAML_UINT_DEFAULT(def, event, emitter, "priority", r->priority, NETPLAN_IP_RULE_PRIO_UNSPEC); YAML_UINT_DEFAULT(def, event, emitter, "type-of-service", r->tos, NETPLAN_IP_RULE_TOS_UNSPEC); YAML_UINT_DEFAULT(def, event, emitter, "mark", r->fwmark, NETPLAN_IP_RULE_FW_MARK_UNSPEC); YAML_STRING(def, event, emitter, "from", r->from); YAML_STRING(def, event, emitter, "to", r->to); YAML_MAPPING_CLOSE(event, emitter); } YAML_SEQUENCE_CLOSE(event, emitter); } return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC gboolean write_openvswitch(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanOVSSettings* ovs, NetplanBackend backend, GHashTable *ovs_ports) { GHashTableIter iter; gpointer key, value; if (has_openvswitch(ovs, backend, ovs_ports)) { YAML_SCALAR_PLAIN(event, emitter, "openvswitch"); YAML_MAPPING_OPEN(event, emitter); if (ovs_ports && g_hash_table_size(ovs_ports) > 0) { YAML_SCALAR_PLAIN(event, emitter, "ports"); YAML_SEQUENCE_OPEN(event, emitter); g_hash_table_iter_init(&iter, ovs_ports); while (g_hash_table_iter_next (&iter, &key, &value)) { YAML_SEQUENCE_OPEN(event, emitter); YAML_SCALAR_PLAIN(event, emitter, key); YAML_SCALAR_PLAIN(event, emitter, value); YAML_SEQUENCE_CLOSE(event, emitter); g_hash_table_iter_remove(&iter); } YAML_SEQUENCE_CLOSE(event, emitter); } if (ovs->external_ids && g_hash_table_size(ovs->external_ids) > 0) { YAML_SCALAR_PLAIN(event, emitter, "external-ids"); YAML_MAPPING_OPEN(event, emitter); g_hash_table_iter_init(&iter, ovs->external_ids); while (g_hash_table_iter_next (&iter, &key, &value)) { YAML_NONNULL_STRING(event, emitter, key, value); } YAML_MAPPING_CLOSE(event, emitter); } if (ovs->other_config && g_hash_table_size(ovs->other_config) > 0) { YAML_SCALAR_PLAIN(event, emitter, "other-config"); YAML_MAPPING_OPEN(event, emitter); g_hash_table_iter_init(&iter, ovs->other_config); while (g_hash_table_iter_next (&iter, &key, &value)) { YAML_NONNULL_STRING(event, emitter, key, value); } YAML_MAPPING_CLOSE(event, emitter); } YAML_NONNULL_STRING(event, emitter, "lacp", ovs->lacp); YAML_NONNULL_STRING(event, emitter, "fail-mode", ovs->fail_mode); if (ovs->mcast_snooping) YAML_NONNULL_STRING_PLAIN(event, emitter, "mcast-snooping", "true"); if (ovs->rstp) YAML_NONNULL_STRING_PLAIN(event, emitter, "rstp", "true"); if (ovs->protocols && ovs->protocols->len > 0) { YAML_SCALAR_PLAIN(event, emitter, "protocols"); YAML_SEQUENCE_OPEN(event, emitter); for (unsigned i = 0; i < ovs->protocols->len; ++i) { const gchar *proto = g_array_index(ovs->protocols, gchar*, i); YAML_SCALAR_PLAIN(event, emitter, proto); } YAML_SEQUENCE_CLOSE(event, emitter); } if (ovs->ssl.ca_certificate || ovs->ssl.client_certificate || ovs->ssl.client_key) { YAML_SCALAR_PLAIN(event, emitter, "ssl"); YAML_MAPPING_OPEN(event, emitter); YAML_NONNULL_STRING(event, emitter, "ca-cert", ovs->ssl.ca_certificate); YAML_NONNULL_STRING(event, emitter, "certificate", ovs->ssl.client_certificate); YAML_NONNULL_STRING(event, emitter, "private-key", ovs->ssl.client_key); YAML_MAPPING_CLOSE(event, emitter); } if (ovs->controller.connection_mode || ovs->controller.addresses) { YAML_SCALAR_PLAIN(event, emitter, "controller"); YAML_MAPPING_OPEN(event, emitter); YAML_NONNULL_STRING(event, emitter, "connection-mode", ovs->controller.connection_mode); if (ovs->controller.addresses) { YAML_SCALAR_PLAIN(event, emitter, "addresses"); YAML_SEQUENCE_OPEN(event, emitter); for (unsigned i = 0; i < ovs->controller.addresses->len; ++i) { const gchar *addr = g_array_index(ovs->controller.addresses, gchar*, i); YAML_SCALAR_QUOTED(event, emitter, addr); } YAML_SEQUENCE_CLOSE(event, emitter); } YAML_MAPPING_CLOSE(event, emitter); } YAML_MAPPING_CLOSE(event, emitter); } return TRUE; err_path: return FALSE; // LCOV_EXCL_LINE } STATIC void _serialize_yaml( const NetplanState* np_state, yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) { GArray* tmp_arr = NULL; GHashTableIter iter; gpointer key, value; YAML_SCALAR_PLAIN(event, emitter, def->id); YAML_MAPPING_OPEN(event, emitter); /* We write out the renderer in very specific circumstances. There's a special case for VLANs, * and unless explicitly specified, we only write out standard renderers if they don't match the global * one or are the default and the global one isn't specified. */ if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) { YAML_NONNULL_STRING_PLAIN(event, emitter, "renderer", "sriov"); } else if (DIRTY(def, def->backend) || ( def->backend != get_default_backend_for_type(np_state->backend, def->type) && def->backend != np_state->backend && def->backend != NETPLAN_BACKEND_OVS)) { YAML_NONNULL_STRING_PLAIN(event, emitter, "renderer", netplan_backend_name(def->backend)); } if (def->has_match) write_match(event, emitter, def); /* Do not try to handle "unknown" connection types (full fallback/passthrough) */ if (def->type == NETPLAN_DEF_TYPE_NM) goto only_passthrough; if (def->optional) YAML_NONNULL_STRING_PLAIN(event, emitter, "optional", "true"); if (def->critical) YAML_NONNULL_STRING_PLAIN(event, emitter, "critical", "true"); if (def->ignore_carrier) YAML_NONNULL_STRING_PLAIN(event, emitter, "ignore-carrier", "true"); if (def->ip4_addresses || def->ip6_addresses || def->address_options) write_addresses(event, emitter, def); if (def->ip4_nameservers || def->ip6_nameservers || def->search_domains) write_nameservers(event, emitter, def); YAML_STRING_PLAIN(def, event, emitter, "gateway4", def->gateway4); YAML_STRING_PLAIN(def, event, emitter, "gateway6", def->gateway6); YAML_STRING(def, event, emitter, "dhcp-identifier", def->dhcp_identifier); YAML_BOOL_TRUE(def, event, emitter, "dhcp4", def->dhcp4); write_dhcp_overrides(event, emitter, "dhcp4-overrides", def, &def->dhcp4_overrides); YAML_BOOL_TRUE(def, event, emitter, "dhcp6", def->dhcp6); write_dhcp_overrides(event, emitter, "dhcp6-overrides", def, &def->dhcp6_overrides); if (def->accept_ra == NETPLAN_RA_MODE_ENABLED) { YAML_NONNULL_STRING_PLAIN(event, emitter, "accept-ra", "true"); } else if (def->accept_ra == NETPLAN_RA_MODE_DISABLED) { YAML_NONNULL_STRING_PLAIN(event, emitter, "accept-ra", "false"); } YAML_STRING(def, event, emitter, "macaddress", def->set_mac); YAML_STRING(def, event, emitter, "set-name", def->set_name); YAML_NONNULL_STRING(event, emitter, "ipv6-address-generation", netplan_addr_gen_mode_name(def->ip6_addr_gen_mode)); YAML_STRING(def, event, emitter, "ipv6-address-token", def->ip6_addr_gen_token); YAML_BOOL_TRUE(def, event, emitter, "ipv6-privacy", def->ip6_privacy); YAML_UINT_0(def, event, emitter, "ipv6-mtu", def->ipv6_mtubytes); YAML_UINT_0(def, event, emitter, "mtu", def->mtubytes); if (def->emit_lldp) YAML_NONNULL_STRING_PLAIN(event, emitter, "emit-lldp", "true"); if (def->has_auth) write_auth(event, emitter, def->auth); /* activation-mode */ YAML_STRING(def, event, emitter, "activation-mode", def->activation_mode); /* SR-IOV */ if (def->sriov_link) YAML_STRING(def, event, emitter, "link", def->sriov_link->id); YAML_UINT_DEFAULT(def, event, emitter, "virtual-function-count", def->sriov_explicit_vf_count, G_MAXUINT); YAML_STRING(def, event, emitter, "embedded-switch-mode", def->embedded_switch_mode); YAML_BOOL_TRUE(def, event, emitter, "delay-virtual-functions-rebind", def->sriov_delay_virtual_functions_rebind); if (def->type == NETPLAN_DEF_TYPE_VETH && def->veth_peer_link) YAML_STRING(def, event, emitter, "peer", def->veth_peer_link->id); /* Search interfaces */ if (def->type == NETPLAN_DEF_TYPE_BRIDGE || def->type == NETPLAN_DEF_TYPE_BOND || def->type == NETPLAN_DEF_TYPE_VRF) { tmp_arr = g_array_new(FALSE, FALSE, sizeof(NetplanNetDefinition*)); g_hash_table_iter_init(&iter, np_state->netdefs); while (g_hash_table_iter_next (&iter, &key, &value)) { NetplanNetDefinition *nd = (NetplanNetDefinition *) value; if (g_strcmp0(nd->bond, def->id) == 0 || g_strcmp0(nd->bridge, def->id) == 0 || nd->vrf_link == def) g_array_append_val(tmp_arr, nd); } if (tmp_arr->len > 0) { YAML_SCALAR_PLAIN(event, emitter, "interfaces"); YAML_SEQUENCE_OPEN(event, emitter); for (unsigned i = 0; i < tmp_arr->len; ++i) { NetplanNetDefinition *nd = g_array_index(tmp_arr, NetplanNetDefinition*, i); YAML_SCALAR_PLAIN(event, emitter, nd->id); } YAML_SEQUENCE_CLOSE(event, emitter); } write_bond_params(event, emitter, def); write_bridge_params(event, emitter, def, tmp_arr); g_array_free(tmp_arr, TRUE); } write_routes(event, emitter, def); YAML_BOOL_TRISTATE(def, event, emitter, "hairpin", def->bridge_hairpin); YAML_BOOL_TRISTATE(def, event, emitter, "port-mac-learning", def->bridge_learning); YAML_BOOL_TRISTATE(def, event, emitter, "neigh-suppress", def->bridge_neigh_suppress); /* VLAN settings */ if (def->type == NETPLAN_DEF_TYPE_VLAN) { YAML_UINT_DEFAULT(def, event, emitter, "id", def->vlan_id, G_MAXUINT); if (def->vlan_link) YAML_STRING(def, event, emitter, "link", def->vlan_link->id); } /* VRF settings */ if (def->type == NETPLAN_DEF_TYPE_VRF) YAML_UINT_DEFAULT(def, event, emitter, "table", def->vrf_table, G_MAXUINT); /* Tunnel settings */ if (def->type == NETPLAN_DEF_TYPE_TUNNEL) { write_tunnel_settings(event, emitter, def); } /* wake-on-lan */ YAML_BOOL_TRUE(def, event, emitter, "wakeonlan", def->wake_on_lan); /* Offload options */ if (def->receive_checksum_offload != NETPLAN_TRISTATE_UNSET) YAML_BOOL_TRUE(def, event, emitter, "receive-checksum-offload", def->receive_checksum_offload); if (def->transmit_checksum_offload != NETPLAN_TRISTATE_UNSET) YAML_BOOL_TRUE(def, event, emitter, "transmit-checksum-offload", def->transmit_checksum_offload); if (def->tcp_segmentation_offload != NETPLAN_TRISTATE_UNSET) YAML_BOOL_TRUE(def, event, emitter, "tcp-segmentation-offload", def->tcp_segmentation_offload); if (def->tcp6_segmentation_offload != NETPLAN_TRISTATE_UNSET) YAML_BOOL_TRUE(def, event, emitter, "tcp6-segmentation-offload", def->tcp6_segmentation_offload); if (def->generic_segmentation_offload != NETPLAN_TRISTATE_UNSET) YAML_BOOL_TRUE(def, event, emitter, "generic-segmentation-offload", def->generic_segmentation_offload); if (def->generic_receive_offload != NETPLAN_TRISTATE_UNSET) YAML_BOOL_TRUE(def, event, emitter, "generic-receive-offload", def->generic_receive_offload); if (def->large_receive_offload != NETPLAN_TRISTATE_UNSET) YAML_BOOL_TRUE(def, event, emitter, "large-receive-offload", def->large_receive_offload); if (def->wowlan && def->wowlan != NETPLAN_WIFI_WOWLAN_DEFAULT) { YAML_SCALAR_PLAIN(event, emitter, "wakeonwlan"); YAML_SEQUENCE_OPEN(event, emitter); /* XXX: make sure to extend if NetplanWifiWowlanFlag is extended */ if (def->wowlan & NETPLAN_WIFI_WOWLAN_ANY) YAML_SCALAR_PLAIN(event, emitter, "any"); if (def->wowlan & NETPLAN_WIFI_WOWLAN_DISCONNECT) YAML_SCALAR_PLAIN(event, emitter, "disconnect"); if (def->wowlan & NETPLAN_WIFI_WOWLAN_MAGIC) YAML_SCALAR_PLAIN(event, emitter, "magic_pkt"); if (def->wowlan & NETPLAN_WIFI_WOWLAN_GTK_REKEY_FAILURE) YAML_SCALAR_PLAIN(event, emitter, "gtk_rekey_failure"); if (def->wowlan & NETPLAN_WIFI_WOWLAN_EAP_IDENTITY_REQ) YAML_SCALAR_PLAIN(event, emitter, "eap_identity_req"); if (def->wowlan & NETPLAN_WIFI_WOWLAN_4WAY_HANDSHAKE) YAML_SCALAR_PLAIN(event, emitter, "four_way_handshake"); if (def->wowlan & NETPLAN_WIFI_WOWLAN_RFKILL_RELEASE) YAML_SCALAR_PLAIN(event, emitter, "rfkill_release"); if (def->wowlan & NETPLAN_WIFI_WOWLAN_TCP) YAML_SCALAR_PLAIN(event, emitter, "tcp"); YAML_SEQUENCE_CLOSE(event, emitter); } YAML_STRING(def, event, emitter, "regulatory-domain", def->regulatory_domain); if (def->optional_addresses) { YAML_SCALAR_PLAIN(event, emitter, "optional-addresses"); YAML_SEQUENCE_OPEN(event, emitter); if (def->optional_addresses & NETPLAN_OPTIONAL_IPV4_LL) YAML_SCALAR_PLAIN(event, emitter, "ipv4-ll") if (def->optional_addresses & NETPLAN_OPTIONAL_IPV6_RA) YAML_SCALAR_PLAIN(event, emitter, "ipv6-ra") if (def->optional_addresses & NETPLAN_OPTIONAL_DHCP4) YAML_SCALAR_PLAIN(event, emitter, "dhcp4") if (def->optional_addresses & NETPLAN_OPTIONAL_DHCP6) YAML_SCALAR_PLAIN(event, emitter, "dhcp6") if (def->optional_addresses & NETPLAN_OPTIONAL_STATIC) YAML_SCALAR_PLAIN(event, emitter, "static") YAML_SEQUENCE_CLOSE(event, emitter); } /* Generate "link-local" if it differs from the default: "[ ipv6 ]" */ if (!(def->linklocal.ipv6 && !def->linklocal.ipv4)) { YAML_SCALAR_PLAIN(event, emitter, "link-local"); YAML_SEQUENCE_OPEN(event, emitter); if (def->linklocal.ipv4) YAML_SCALAR_PLAIN(event, emitter, "ipv4"); if (def->linklocal.ipv6) YAML_SCALAR_PLAIN(event, emitter, "ipv6"); YAML_SEQUENCE_CLOSE(event, emitter); } write_openvswitch(event, emitter, &def->ovs_settings, def->backend, NULL); /* InfiniBand */ if (def->ib_mode != NETPLAN_IB_MODE_KERNEL) { const char* ib_mode_str = netplan_infiniband_mode_name(def->ib_mode); YAML_STRING(def, event, emitter, "infiniband-mode", ib_mode_str); } if (def->type == NETPLAN_DEF_TYPE_MODEM) write_modem_params(event, emitter, def); if (def->type == NETPLAN_DEF_TYPE_WIFI) if (!write_access_points(event, emitter, def)) goto err_path; /* Handle devices in full fallback/passthrough mode (i.e. 'nm-devices') */ only_passthrough: if (!write_backend_settings(event, emitter, def->backend_settings)) goto err_path; /* Close remaining mappings */ YAML_MAPPING_CLOSE(event, emitter); return; // LCOV_EXCL_START err_path: g_warning("Error generating YAML: %s", emitter->problem); return; // LCOV_EXCL_STOP } gboolean netplan_netdef_write_yaml( const NetplanState* np_state, const NetplanNetDefinition* netdef, const char* rootdir, GError** error) { g_autofree gchar *filename = NULL; g_autofree gchar *path = NULL; mode_t orig_umask; /* NetworkManager produces one file per connection profile * It's 90-* to be higher priority than the default 70-netplan-set.yaml */ if (netdef->backend_settings.uuid) filename = g_strconcat("90-NM-", netdef->backend_settings.uuid, ".yaml", NULL); else filename = g_strconcat("10-netplan-", netdef->id, ".yaml", NULL); path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, "etc", "netplan", filename, NULL); /* Start rendering YAML output */ yaml_emitter_t emitter_data; yaml_event_t event_data; yaml_emitter_t* emitter = &emitter_data; yaml_event_t* event = &event_data; orig_umask = umask(077); // owner (root) read-only FILE *output = fopen(path, "wb"); umask(orig_umask); YAML_OUT_START(event, emitter, output); /* build the netplan boilerplate YAML structure */ YAML_SCALAR_PLAIN(event, emitter, "network"); YAML_MAPPING_OPEN(event, emitter); YAML_NONNULL_STRING_PLAIN(event, emitter, "version", "2"); if (netplan_def_type_name(netdef->type)) { YAML_SCALAR_PLAIN(event, emitter, netplan_def_type_name(netdef->type)); YAML_MAPPING_OPEN(event, emitter); _serialize_yaml(np_state, event, emitter, netdef); YAML_MAPPING_CLOSE(event, emitter); } /* Close remaining mappings */ YAML_MAPPING_CLOSE(event, emitter); /* Tear down the YAML emitter */ YAML_OUT_STOP(event, emitter); fclose(output); return TRUE; // LCOV_EXCL_START err_path: g_set_error(error, NETPLAN_EMITTER_ERROR, NETPLAN_ERROR_YAML_EMITTER, "Error generating YAML: %s", emitter->problem); yaml_emitter_delete(emitter); fclose(output); return FALSE; // LCOV_EXCL_STOP } STATIC int contains_netdef_type(gconstpointer value, gconstpointer user_data) { const NetplanNetDefinition *nd = value; const NetplanDefType *type = user_data; return nd->type == *type ? 0 : -1; } STATIC gboolean netplan_netdef_list_write_yaml(const NetplanState* np_state, GList* netdefs, int out_fd, const char* out_fname, gboolean is_fallback, GError** error) { GHashTable *ovs_ports = NULL; int dup_fd = dup(out_fd); if (dup_fd < 0) goto file_error; // LCOV_EXCL_LINE FILE* out_stream = fdopen(dup_fd, "w"); if (!out_stream) goto file_error; /* Start rendering YAML output */ yaml_emitter_t emitter_data; yaml_event_t event_data; yaml_emitter_t* emitter = &emitter_data; yaml_event_t* event = &event_data; YAML_OUT_START(event, emitter, out_stream); /* build the netplan boilerplate YAML structure */ YAML_SCALAR_PLAIN(event, emitter, "network"); YAML_MAPPING_OPEN(event, emitter); /* We support version 2 only, currently */ YAML_NONNULL_STRING_PLAIN(event, emitter, "version", "2"); /* fallback to default global handling, if renderer was not set for this file */ NetplanBackend renderer = netplan_state_get_backend(np_state); /* Try to find a file specific (global) renderer. * If this is the fallback file (70-netplan-set.yaml or .yaml), * a renderer parsed from a YAML patch takes precedence. */ if (out_fname && np_state->global_renderer) { gpointer value; renderer = GPOINTER_TO_INT(g_hash_table_lookup(np_state->global_renderer, out_fname)); /* A renderer parsed from an (anonymous) YAML patch takes precendence * (e.g. "netplan set ..."). Such data does not have any filename * associated to it in the global_renderer map (i.e. empty string). */ if (is_fallback && g_hash_table_lookup_extended(np_state->global_renderer, "", NULL, &value)) renderer = GPOINTER_TO_INT(value); } if (renderer == NETPLAN_BACKEND_NM || renderer == NETPLAN_BACKEND_NETWORKD) YAML_NONNULL_STRING_PLAIN(event, emitter, "renderer", netplan_backend_name(renderer)); /* Do not write any netdefs, if we're just setting/updating some globals, * e.g.: netplan set "network.renderer=NetworkManager" */ if (!netdefs) goto skip_netdefs; /* Go through the netdefs type-by-type */ for (unsigned i = 0; i < NETPLAN_DEF_TYPE_MAX_; ++i) { if (i == NETPLAN_DEF_TYPE_NM_PLACEHOLDER_) continue; /* Per-netdef config */ if (g_list_find_custom(netdefs, &i, contains_netdef_type)) { if (i == NETPLAN_DEF_TYPE_PORT) { GList* iter = netdefs; while (iter) { NetplanNetDefinition *def = iter->data; if (def->type == i) { if (!ovs_ports) ovs_ports = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); /* Check that the peer hasn't already been inserted to avoid duplication */ if (!g_hash_table_lookup(ovs_ports, def->peer)) g_hash_table_insert(ovs_ports, g_strdup(def->id), g_strdup(def->peer)); } iter = g_list_next(iter); } } else if (netplan_def_type_name(i)) { GList* iter = netdefs; YAML_SCALAR_PLAIN(event, emitter, netplan_def_type_name(i)); YAML_MAPPING_OPEN(event, emitter); while (iter) { NetplanNetDefinition *def = iter->data; if (def->type == i) _serialize_yaml(np_state, event, emitter, def); iter = g_list_next(iter); } YAML_MAPPING_CLOSE(event, emitter); } } } skip_netdefs: write_openvswitch(event, emitter, &np_state->ovs_settings, NETPLAN_BACKEND_NONE, ovs_ports); /* Close remaining mappings */ YAML_MAPPING_CLOSE(event, emitter); /* Tear down the YAML emitter */ YAML_OUT_STOP(event, emitter); fclose(out_stream); return TRUE; // LCOV_EXCL_START err_path: g_set_error(error, NETPLAN_EMITTER_ERROR, NETPLAN_ERROR_YAML_EMITTER, "Error generating YAML: %s", emitter->problem); yaml_emitter_delete(emitter); fclose(out_stream); return FALSE; // LCOV_EXCL_STOP file_error: g_set_error(error, NETPLAN_FILE_ERROR, errno, "%m"); return FALSE; } gboolean netplan_state_write_yaml_file(const NetplanState* np_state, const char* filename, const char* rootdir, GError** error) { GList* iter = np_state->netdefs_ordered; g_autofree gchar* path = NULL; g_autofree gchar* tmp_path = NULL; GList* to_write = NULL; int out_fd; path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, "etc", "netplan", filename, NULL); while (iter) { NetplanNetDefinition* netdef = iter->data; const char* fname = netdef->filepath ? netdef->filepath : path; if (g_strcmp0(fname, path) == 0) to_write = g_list_append(to_write, netdef); iter = iter->next; } /* Remove any existing file if there is no data to write */ gboolean write_globals = !!np_state->global_renderer; if (to_write == NULL && !write_globals) { if (unlink(path) && errno != ENOENT) { g_set_error(error, NETPLAN_FILE_ERROR, errno, "%m"); return FALSE; } return TRUE; } tmp_path = g_strdup_printf("%s.XXXXXX", path); /* * glibc will create a file with mode 600 by default. * Although, mkstemp manpage says that the POSIX spec doesn't say anything * about file modes and the application should make sure umask is set * appropriately before calling mkstemp(). */ mode_t old_umask = umask(077); out_fd = mkstemp(tmp_path); // permissions 0600 by default umask(old_umask); if (out_fd < 0) { g_set_error(error, NETPLAN_FILE_ERROR, errno, "%m"); return FALSE; } gboolean ret = netplan_netdef_list_write_yaml(np_state, to_write, out_fd, path, TRUE, error); g_list_free(to_write); close(out_fd); if (ret) { if (rename(tmp_path, path) == 0) return TRUE; g_set_error(error, NETPLAN_FILE_ERROR, errno, "%m"); } /* Something went wrong, clean up the tempfile! */ unlink(tmp_path); return FALSE; } gboolean netplan_state_dump_yaml(const NetplanState* np_state, int out_fd, GError** error) { if (!np_state->netdefs_ordered && !netplan_state_has_nondefault_globals(np_state)) return TRUE; return netplan_netdef_list_write_yaml(np_state, np_state->netdefs_ordered, out_fd, NULL, TRUE, error); } gboolean netplan_state_update_yaml_hierarchy(const NetplanState* np_state, const char* default_filename, const char* rootdir, GError** error) { g_autofree gchar *default_path = NULL; gboolean ret = FALSE; GHashTableIter hash_iter; gpointer key, value; GHashTable *perfile_netdefs; g_assert(default_filename != NULL && *default_filename != '\0'); perfile_netdefs = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_list_free); default_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, "etc", "netplan", default_filename, NULL); int out_fd = -1; /* Dump global conf to the default path */ if (!np_state->netdefs || g_hash_table_size(np_state->netdefs) == 0) { if ( has_openvswitch(&np_state->ovs_settings, NETPLAN_BACKEND_NONE, NULL) || (np_state->backend != NETPLAN_BACKEND_NONE && np_state->global_renderer && ( g_hash_table_contains(np_state->global_renderer, default_path) // 70-netplan-set.yaml already exsits and defines a global renderer || g_hash_table_contains(np_state->global_renderer, "")))) { // 70-netplan-set.yaml doesn't exist, but we need to create it to define a global renderer g_hash_table_insert(perfile_netdefs, default_path, NULL); } } else { GList* iter = np_state->netdefs_ordered; while (iter) { NetplanNetDefinition* netdef = iter->data; const char* filename = netdef->filepath ? netdef->filepath : default_path; GList* list = NULL; g_hash_table_steal_extended(perfile_netdefs, filename, NULL, (gpointer*)&list); g_hash_table_insert(perfile_netdefs, (gpointer)filename, g_list_append(list, netdef)); iter = iter->next; } } /* Add files containing a global renderer value to "perfile_netdefs", so * they are updated on disk. */ if (np_state->global_renderer && g_hash_table_size(np_state->global_renderer) > 0) { g_hash_table_iter_init(&hash_iter, np_state->global_renderer); while (g_hash_table_iter_next (&hash_iter, &key, &value)) { char *filename = key; /* Anonymous globals will go to the default YAML (see above) */ if (g_strcmp0(filename, "") == 0) continue; /* Ignore the update of this file if it's already going to be * written, caused by updated netdefs. */ if (!g_hash_table_contains(perfile_netdefs, filename)) g_hash_table_insert(perfile_netdefs, filename, NULL); } } g_hash_table_iter_init(&hash_iter, perfile_netdefs); while (g_hash_table_iter_next (&hash_iter, &key, &value)) { const char *filename = key; gboolean is_fallback = (g_strcmp0(filename, default_path) == 0); GList* netdefs = value; out_fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (out_fd < 0) goto file_error; if (!netplan_netdef_list_write_yaml(np_state, netdefs, out_fd, filename, is_fallback, error)) goto cleanup; // LCOV_EXCL_LINE close(out_fd); out_fd = -1; } /* Remove any referenced source file that doesn't have any associated data. Presumably, it is data that has been obsoleted by files loaded afterwards, typically via `netplan set`. */ if (np_state->sources) { g_hash_table_iter_init(&hash_iter, np_state->sources); while (g_hash_table_iter_next (&hash_iter, &key, &value)) { if (!g_hash_table_contains(perfile_netdefs, key)) { if (unlink(key) && errno != ENOENT) goto file_error; // LCOV_EXCL_LINE } } } ret = TRUE; goto cleanup; file_error: g_set_error(error, NETPLAN_FILE_ERROR, errno, "%m"); ret = FALSE; cleanup: if (out_fd >= 0) close(out_fd); // LCOV_EXCL_LINE g_hash_table_destroy(perfile_netdefs); return ret; } netplan-1.0/src/netplan.script000077500000000000000000000014421457004145200164770ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . '''netplan command line''' from netplan_cli import Netplan netplan = Netplan() netplan.main() netplan-1.0/src/networkd.c000066400000000000000000001647661457004145200156310ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * Author: Martin Pitt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include "networkd.h" #include "parse.h" #include "names.h" #include "util.h" #include "util-internal.h" #include "validation.h" /** * Append WiFi frequencies to wpa_supplicant's freq_list= */ STATIC void wifi_append_freq(__unused gpointer key, gpointer value, gpointer user_data) { GString* s = user_data; g_string_append_printf(s, "%d ", GPOINTER_TO_INT(value)); } /** * append wowlan_triggers= string for wpa_supplicant.conf */ STATIC gboolean append_wifi_wowlan_flags(NetplanWifiWowlanFlag flag, GString* str, GError** error) { if (flag & NETPLAN_WIFI_WOWLAN_TYPES[0].flag || flag >= NETPLAN_WIFI_WOWLAN_TCP) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: unsupported wowlan_triggers mask: 0x%x\n", flag); return FALSE; } for (unsigned i = 0; NETPLAN_WIFI_WOWLAN_TYPES[i].name != NULL; ++i) { if (flag & NETPLAN_WIFI_WOWLAN_TYPES[i].flag) { g_string_append_printf(str, "%s ", NETPLAN_WIFI_WOWLAN_TYPES[i].name); } } /* replace trailing space with newline */ str = g_string_overwrite(str, str->len-1, "\n"); return TRUE; } /** * Append [Match] section of @def to @s. */ STATIC void append_match_section(const NetplanNetDefinition* def, GString* s, gboolean match_rename) { /* Note: an empty [Match] section is interpreted as matching all devices, * which is what we want for the simple case that you only have one device * (of the given type) */ g_string_append(s, "[Match]\n"); if (def->match.driver && strchr(def->match.driver, '\t')) { gchar **split = g_strsplit(def->match.driver, "\t", 0); g_string_append_printf(s, "Driver=%s", split[0]); for (unsigned i = 1; split[i]; ++i) g_string_append_printf(s, " %s", split[i]); g_string_append(s, "\n"); g_strfreev(split); } else if (def->match.driver) g_string_append_printf(s, "Driver=%s\n", def->match.driver); if (def->match.mac) { /* LP: #1804861 and LP: #1888726: * Using bond, bridge, and VLAN devices results in sharing MAC * addresses across interfaces. Match by PermanentMACAddress to match * only the real phy interface and to continue to match it even after * its MAC address has been changed. */ g_string_append_printf(s, "PermanentMACAddress=%s\n", def->match.mac); } /* name matching is special: if the .link renames the interface, the * .network has to use the renamed one, otherwise the original one */ if (!match_rename && def->match.original_name) g_string_append_printf(s, "OriginalName=%s\n", def->match.original_name); if (match_rename) { if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL) g_string_append_printf(s, "Name=%s\n", def->id); else if (def->set_name) g_string_append_printf(s, "Name=%s\n", def->set_name); else if (def->match.original_name) g_string_append_printf(s, "Name=%s\n", def->match.original_name); } } STATIC void write_bridge_params_networkd(GString* s, const NetplanNetDefinition* def) { GString *params = NULL; if (def->custom_bridging) { params = g_string_sized_new(200); if (def->bridge_params.ageing_time) g_string_append_printf(params, "AgeingTimeSec=%s\n", def->bridge_params.ageing_time); if (def->bridge_params.priority) g_string_append_printf(params, "Priority=%u\n", def->bridge_params.priority); if (def->bridge_params.forward_delay) g_string_append_printf(params, "ForwardDelaySec=%s\n", def->bridge_params.forward_delay); if (def->bridge_params.hello_time) g_string_append_printf(params, "HelloTimeSec=%s\n", def->bridge_params.hello_time); if (def->bridge_params.max_age) g_string_append_printf(params, "MaxAgeSec=%s\n", def->bridge_params.max_age); g_string_append_printf(params, "STP=%s\n", def->bridge_params.stp ? "true" : "false"); g_string_append_printf(s, "\n[Bridge]\n%s", params->str); g_string_free(params, TRUE); } } STATIC void write_tunnel_params(GString* s, const NetplanNetDefinition* def) { GString *params = NULL; params = g_string_sized_new(200); g_string_printf(params, "Independent=true\n"); if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_IPIP6 || def->tunnel.mode == NETPLAN_TUNNEL_MODE_IP6IP6) g_string_append_printf(params, "Mode=%s\n", netplan_tunnel_mode_name(def->tunnel.mode)); if (def->tunnel.local_ip) g_string_append_printf(params, "Local=%s\n", def->tunnel.local_ip); g_string_append_printf(params, "Remote=%s\n", def->tunnel.remote_ip); if (def->tunnel_ttl) g_string_append_printf(params, "TTL=%u\n", def->tunnel_ttl); if (def->tunnel.input_key) g_string_append_printf(params, "InputKey=%s\n", def->tunnel.input_key); if (def->tunnel.output_key) g_string_append_printf(params, "OutputKey=%s\n", def->tunnel.output_key); g_string_append_printf(s, "\n[Tunnel]\n%s", params->str); g_string_free(params, TRUE); } STATIC void write_wireguard_params(GString* s, const NetplanNetDefinition* def) { GString *params = NULL; params = g_string_sized_new(200); g_assert(def->tunnel.private_key); /* The "PrivateKeyFile=" setting is available as of systemd-netwokrd v242+ * Base64 encoded PrivateKey= or absolute PrivateKeyFile= fields are mandatory. * * The key was already validated via validate_tunnel_grammar(), but we need * to differentiate between base64 key VS absolute path key-file. And a base64 * string could (theoretically) start with '/', so we use is_wireguard_key() * as well to check for more specific characteristics (if needed). */ if (def->tunnel.private_key[0] == '/' && !is_wireguard_key(def->tunnel.private_key)) g_string_append_printf(params, "PrivateKeyFile=%s\n", def->tunnel.private_key); else g_string_append_printf(params, "PrivateKey=%s\n", def->tunnel.private_key); if (def->tunnel.port) g_string_append_printf(params, "ListenPort=%u\n", def->tunnel.port); /* This is called FirewallMark= as of systemd v243, but we keep calling it FwMark= for backwards compatibility. FwMark= is still supported, but deprecated: https://github.com/systemd/systemd/pull/12478 */ if (def->tunnel.fwmark) g_string_append_printf(params, "FwMark=%u\n", def->tunnel.fwmark); g_string_append_printf(s, "\n[WireGuard]\n%s", params->str); g_string_free(params, TRUE); for (guint i = 0; i < def->wireguard_peers->len; i++) { NetplanWireguardPeer *peer = g_array_index (def->wireguard_peers, NetplanWireguardPeer*, i); GString *peer_s = g_string_sized_new(200); g_string_append_printf(peer_s, "PublicKey=%s\n", peer->public_key); g_string_append(peer_s, "AllowedIPs="); for (guint i = 0; i < peer->allowed_ips->len; ++i) { if (i > 0 ) g_string_append_c(peer_s, ','); g_string_append_printf(peer_s, "%s", g_array_index(peer->allowed_ips, char*, i)); } g_string_append_c(peer_s, '\n'); if (peer->keepalive) g_string_append_printf(peer_s, "PersistentKeepalive=%d\n", peer->keepalive); if (peer->endpoint) g_string_append_printf(peer_s, "Endpoint=%s\n", peer->endpoint); /* The key was already validated via validate_tunnel_grammar(), but we need * to differentiate between base64 key VS absolute path key-file. And a base64 * string could (theoretically) start with '/', so we use is_wireguard_key() * as well to check for more specific characteristics (if needed). */ if (peer->preshared_key) { if (peer->preshared_key[0] == '/' && !is_wireguard_key(peer->preshared_key)) g_string_append_printf(peer_s, "PresharedKeyFile=%s\n", peer->preshared_key); else g_string_append_printf(peer_s, "PresharedKey=%s\n", peer->preshared_key); } g_string_append_printf(s, "\n[WireGuardPeer]\n%s", peer_s->str); g_string_free(peer_s, TRUE); } } STATIC void write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char* path) { GString* s = NULL; mode_t orig_umask; /* Don't write .link files for virtual devices; they use .netdev instead. * Don't write .link files for MODEM devices, as they aren't supported by networkd. */ if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL || def->type == NETPLAN_DEF_TYPE_MODEM) return; /* do we need to write a .link file? */ if (!def->set_name && !def->wake_on_lan && !def->mtubytes && !(_is_macaddress_special_nd_option(def->set_mac) && def->backend == NETPLAN_BACKEND_NETWORKD) && (def->receive_checksum_offload == NETPLAN_TRISTATE_UNSET) && (def->transmit_checksum_offload == NETPLAN_TRISTATE_UNSET) && (def->tcp_segmentation_offload == NETPLAN_TRISTATE_UNSET) && (def->tcp6_segmentation_offload == NETPLAN_TRISTATE_UNSET) && (def->generic_segmentation_offload == NETPLAN_TRISTATE_UNSET) && (def->generic_receive_offload == NETPLAN_TRISTATE_UNSET) && (def->large_receive_offload == NETPLAN_TRISTATE_UNSET)) return; /* build file contents */ s = g_string_sized_new(200); append_match_section(def, s, FALSE); g_string_append(s, "\n[Link]\n"); if (def->set_name) g_string_append_printf(s, "Name=%s\n", def->set_name); /* FIXME: Should this be turned from bool to str and support multiple values? */ g_string_append_printf(s, "WakeOnLan=%s\n", def->wake_on_lan ? "magic" : "off"); if (def->mtubytes) g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); if (_is_macaddress_special_nd_option(def->set_mac) && def->backend == NETPLAN_BACKEND_NETWORKD) { if (!g_strcmp0(def->set_mac, "permanent")) { /* "permanent" is used for both NM and ND, but the actual setting value for ND is "persistent" */ g_string_append_printf(s, "MACAddressPolicy=persistent\n"); } else { g_string_append_printf(s, "MACAddressPolicy=%s\n", def->set_mac); } } /* Offload options */ if (def->receive_checksum_offload != NETPLAN_TRISTATE_UNSET) g_string_append_printf(s, "ReceiveChecksumOffload=%s\n", (def->receive_checksum_offload ? "true" : "false")); if (def->transmit_checksum_offload != NETPLAN_TRISTATE_UNSET) g_string_append_printf(s, "TransmitChecksumOffload=%s\n", (def->transmit_checksum_offload ? "true" : "false")); if (def->tcp_segmentation_offload != NETPLAN_TRISTATE_UNSET) g_string_append_printf(s, "TCPSegmentationOffload=%s\n", (def->tcp_segmentation_offload ? "true" : "false")); if (def->tcp6_segmentation_offload != NETPLAN_TRISTATE_UNSET) g_string_append_printf(s, "TCP6SegmentationOffload=%s\n", (def->tcp6_segmentation_offload ? "true" : "false")); if (def->generic_segmentation_offload != NETPLAN_TRISTATE_UNSET) g_string_append_printf(s, "GenericSegmentationOffload=%s\n", (def->generic_segmentation_offload ? "true" : "false")); if (def->generic_receive_offload != NETPLAN_TRISTATE_UNSET) g_string_append_printf(s, "GenericReceiveOffload=%s\n", (def->generic_receive_offload ? "true" : "false")); if (def->large_receive_offload != NETPLAN_TRISTATE_UNSET) g_string_append_printf(s, "LargeReceiveOffload=%s\n", (def->large_receive_offload ? "true" : "false")); orig_umask = umask(022); _netplan_g_string_free_to_file(s, rootdir, path, ".link"); umask(orig_umask); } STATIC gboolean write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** error) { g_assert(def->regulatory_domain); g_autofree gchar* id_escaped = NULL; g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/network.target.wants/netplan-regdom.service", NULL); g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-regdom.service", NULL); GString* s = g_string_new("[Unit]\n"); g_string_append(s, "Description=Netplan regulatory-domain configuration\n"); g_string_append(s, "After=network.target\n"); g_string_append(s, "ConditionFileIsExecutable="SBINDIR"/iw\n"); g_string_append(s, "\n[Service]\nType=oneshot\n"); g_string_append_printf(s, "ExecStart="SBINDIR"/iw reg set %s\n", def->regulatory_domain); _netplan_g_string_free_to_file(s, rootdir, path, NULL); _netplan_safe_mkdir_p_dir(link); if (symlink(path, link) < 0 && errno != EEXIST) { // LCOV_EXCL_START g_set_error(error, NETPLAN_FILE_ERROR, errno, "failed to create enablement symlink: %m\n"); return FALSE; // LCOV_EXCL_STOP } return TRUE; } STATIC gboolean interval_has_suffix(const char* param) { gchar* endptr; g_ascii_strtoull(param, &endptr, 10); if (*endptr == '\0') return FALSE; return TRUE; } STATIC void write_bond_parameters(const NetplanNetDefinition* def, GString* s) { GString* params = NULL; params = g_string_sized_new(200); if (def->bond_params.mode) g_string_append_printf(params, "\nMode=%s", def->bond_params.mode); if (def->bond_params.lacp_rate) g_string_append_printf(params, "\nLACPTransmitRate=%s", def->bond_params.lacp_rate); if (def->bond_params.monitor_interval) { g_string_append(params, "\nMIIMonitorSec="); if (interval_has_suffix(def->bond_params.monitor_interval)) g_string_append(params, def->bond_params.monitor_interval); else g_string_append_printf(params, "%sms", def->bond_params.monitor_interval); } if (def->bond_params.min_links) g_string_append_printf(params, "\nMinLinks=%d", def->bond_params.min_links); if (def->bond_params.transmit_hash_policy) g_string_append_printf(params, "\nTransmitHashPolicy=%s", def->bond_params.transmit_hash_policy); if (def->bond_params.selection_logic) g_string_append_printf(params, "\nAdSelect=%s", def->bond_params.selection_logic); if (def->bond_params.all_members_active) g_string_append_printf(params, "\nAllSlavesActive=%d", def->bond_params.all_members_active); /* wokeignore:rule=slave */ if (def->bond_params.arp_interval) { g_string_append(params, "\nARPIntervalSec="); if (interval_has_suffix(def->bond_params.arp_interval)) g_string_append(params, def->bond_params.arp_interval); else g_string_append_printf(params, "%sms", def->bond_params.arp_interval); } if (def->bond_params.arp_ip_targets && def->bond_params.arp_ip_targets->len > 0) { g_string_append_printf(params, "\nARPIPTargets="); for (unsigned i = 0; i < def->bond_params.arp_ip_targets->len; ++i) { if (i > 0) g_string_append_printf(params, " "); g_string_append_printf(params, "%s", g_array_index(def->bond_params.arp_ip_targets, char*, i)); } } if (def->bond_params.arp_validate) g_string_append_printf(params, "\nARPValidate=%s", def->bond_params.arp_validate); if (def->bond_params.arp_all_targets) g_string_append_printf(params, "\nARPAllTargets=%s", def->bond_params.arp_all_targets); if (def->bond_params.up_delay) { g_string_append(params, "\nUpDelaySec="); if (interval_has_suffix(def->bond_params.up_delay)) g_string_append(params, def->bond_params.up_delay); else g_string_append_printf(params, "%sms", def->bond_params.up_delay); } if (def->bond_params.down_delay) { g_string_append(params, "\nDownDelaySec="); if (interval_has_suffix(def->bond_params.down_delay)) g_string_append(params, def->bond_params.down_delay); else g_string_append_printf(params, "%sms", def->bond_params.down_delay); } if (def->bond_params.fail_over_mac_policy) g_string_append_printf(params, "\nFailOverMACPolicy=%s", def->bond_params.fail_over_mac_policy); if (def->bond_params.gratuitous_arp) g_string_append_printf(params, "\nGratuitousARP=%d", def->bond_params.gratuitous_arp); /* TODO: add unsolicited_na, not documented as supported by NM. */ if (def->bond_params.packets_per_member) g_string_append_printf(params, "\nPacketsPerSlave=%d", def->bond_params.packets_per_member); /* wokeignore:rule=slave */ if (def->bond_params.primary_reselect_policy) g_string_append_printf(params, "\nPrimaryReselectPolicy=%s", def->bond_params.primary_reselect_policy); if (def->bond_params.resend_igmp) g_string_append_printf(params, "\nResendIGMP=%d", def->bond_params.resend_igmp); if (def->bond_params.learn_interval) g_string_append_printf(params, "\nLearnPacketIntervalSec=%s", def->bond_params.learn_interval); if (params->len) g_string_append_printf(s, "\n[Bond]%s\n", params->str); g_string_free(params, TRUE); } STATIC void write_vxlan_parameters(const NetplanNetDefinition* def, GString* s) { g_assert(def->vxlan); GString* params = NULL; params = g_string_sized_new(200); if (def->tunnel.remote_ip) { if (is_multicast_address(def->tunnel.remote_ip)) g_string_append_printf(params, "\nGroup=%s", def->tunnel.remote_ip); else g_string_append_printf(params, "\nRemote=%s", def->tunnel.remote_ip); } if (def->tunnel.local_ip) g_string_append_printf(params, "\nLocal=%s", def->tunnel.local_ip); if (def->vxlan->tos) g_string_append_printf(params, "\nTOS=%d", def->vxlan->tos); if (def->tunnel_ttl) g_string_append_printf(params, "\nTTL=%d", def->tunnel_ttl); if (def->vxlan->mac_learning != NETPLAN_TRISTATE_UNSET) g_string_append_printf(params, "\nMacLearning=%s", def->vxlan->mac_learning ? "true" : "false"); if (def->vxlan->ageing) g_string_append_printf(params, "\nFDBAgeingSec=%d", def->vxlan->ageing); if (def->vxlan->limit) g_string_append_printf(params, "\nMaximumFDBEntries=%d", def->vxlan->limit); if (def->vxlan->arp_proxy != NETPLAN_TRISTATE_UNSET) g_string_append_printf(params, "\nReduceARPProxy=%s", def->vxlan->arp_proxy ? "true" : "false"); if (def->vxlan->notifications) { if (def->vxlan->notifications & NETPLAN_VXLAN_NOTIFICATION_L2_MISS) g_string_append(params, "\nL2MissNotification=true"); if (def->vxlan->notifications & NETPLAN_VXLAN_NOTIFICATION_L3_MISS) g_string_append(params, "\nL3MissNotification=true"); } if (def->vxlan->short_circuit != NETPLAN_TRISTATE_UNSET) g_string_append_printf(params, "\nRouteShortCircuit=%s", def->vxlan->short_circuit ? "true" : "false"); if (def->vxlan->checksums) { if (def->vxlan->checksums & NETPLAN_VXLAN_CHECKSUM_UDP) g_string_append(params, "\nUDPChecksum=true"); if (def->vxlan->checksums & NETPLAN_VXLAN_CHECKSUM_ZERO_UDP6_TX) g_string_append(params, "\nUDP6ZeroChecksumTx=true"); if (def->vxlan->checksums & NETPLAN_VXLAN_CHECKSUM_ZERO_UDP6_RX) g_string_append(params, "\nUDP6ZeroChecksumRx=true"); if (def->vxlan->checksums & NETPLAN_VXLAN_CHECKSUM_REMOTE_TX) g_string_append(params, "\nRemoteChecksumTx=true"); if (def->vxlan->checksums & NETPLAN_VXLAN_CHECKSUM_REMOTE_RX) g_string_append(params, "\nRemoteChecksumRx=true"); } if (def->vxlan->extensions) { if (def->vxlan->extensions & NETPLAN_VXLAN_EXTENSION_GROUP_POLICY) g_string_append(params, "\nGroupPolicyExtension=true"); if (def->vxlan->extensions & NETPLAN_VXLAN_EXTENSION_GENERIC_PROTOCOL) g_string_append(params, "\nGenericProtocolExtension=true"); } if (def->tunnel.port) g_string_append_printf(params, "\nDestinationPort=%d", def->tunnel.port); if (def->vxlan->source_port_min && def->vxlan->source_port_max) g_string_append_printf(params, "\nPortRange=%u-%u", def->vxlan->source_port_min, def->vxlan->source_port_max); if (def->vxlan->flow_label != G_MAXUINT) g_string_append_printf(params, "\nFlowLabel=%d", def->vxlan->flow_label); if (def->vxlan->do_not_fragment != NETPLAN_TRISTATE_UNSET) g_string_append_printf(params, "\nIPDoNotFragment=%s", def->vxlan->do_not_fragment ? "true" : "false"); if (!def->vxlan->link) g_string_append(params, "\nIndependent=true"); if (params->len) g_string_append_printf(s, "%s\n", params->str); g_string_free(params, TRUE); } STATIC void write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const char* path) { GString* s = NULL; mode_t orig_umask; g_assert(def->type >= NETPLAN_DEF_TYPE_VIRTUAL); if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) { g_debug("%s is defined as a hardware SR-IOV filtered VLAN, postponing creation", def->id); return; } /* build file contents */ s = g_string_sized_new(200); g_string_append_printf(s, "[NetDev]\nName=%s\n", def->id); if (def->set_mac && _is_valid_macaddress(def->set_mac)) g_string_append_printf(s, "MACAddress=%s\n", def->set_mac); if (def->mtubytes) g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); switch (def->type) { case NETPLAN_DEF_TYPE_BRIDGE: g_string_append(s, "Kind=bridge\n"); write_bridge_params_networkd(s, def); break; case NETPLAN_DEF_TYPE_BOND: g_string_append(s, "Kind=bond\n"); write_bond_parameters(def, s); break; case NETPLAN_DEF_TYPE_VLAN: g_string_append_printf(s, "Kind=vlan\n\n[VLAN]\nId=%u\n", def->vlan_id); break; case NETPLAN_DEF_TYPE_VRF: g_string_append_printf(s, "Kind=vrf\n\n[VRF]\nTable=%u\n", def->vrf_table); break; case NETPLAN_DEF_TYPE_DUMMY: /* wokeignore:rule=dummy */ g_string_append_printf(s, "Kind=dummy\n"); /* wokeignore:rule=dummy */ break; case NETPLAN_DEF_TYPE_VETH: /* * Only one .netdev file is required to create the veth pair. * To select what netdef we are going to use, we sort both names, get the first one, * and, if the selected name is the name of the netdef being written, we generate * the .netdev file. Otherwise we skip the netdef. */ gchar* first = g_strcmp0(def->id, def->veth_peer_link->id) < 0 ? def->id : def->veth_peer_link->id; if (first != def->id) { g_string_free(s, TRUE); return; } g_string_append_printf(s, "Kind=veth\n\n[Peer]\nName=%s\n", def->veth_peer_link->id); break; case NETPLAN_DEF_TYPE_TUNNEL: switch(def->tunnel.mode) { case NETPLAN_TUNNEL_MODE_GRE: case NETPLAN_TUNNEL_MODE_GRETAP: case NETPLAN_TUNNEL_MODE_IPIP: case NETPLAN_TUNNEL_MODE_IP6GRE: case NETPLAN_TUNNEL_MODE_IP6GRETAP: case NETPLAN_TUNNEL_MODE_SIT: case NETPLAN_TUNNEL_MODE_VTI: case NETPLAN_TUNNEL_MODE_VTI6: case NETPLAN_TUNNEL_MODE_WIREGUARD: g_string_append_printf(s, "Kind=%s\n", netplan_tunnel_mode_name(def->tunnel.mode)); break; case NETPLAN_TUNNEL_MODE_VXLAN: g_string_append_printf(s, "Kind=vxlan\n\n[VXLAN]\nVNI=%u", def->vxlan->vni); break; case NETPLAN_TUNNEL_MODE_IP6IP6: case NETPLAN_TUNNEL_MODE_IPIP6: g_string_append(s, "Kind=ip6tnl\n"); break; // LCOV_EXCL_START default: g_assert_not_reached(); // LCOV_EXCL_STOP } if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) write_wireguard_params(s, def); else if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_VXLAN) write_vxlan_parameters(def, s); else write_tunnel_params(s, def); break; default: g_assert_not_reached(); // LCOV_EXCL_LINE } /* these do not contain secrets and need to be readable by * systemd-networkd - LP: #1736965 */ orig_umask = umask(022); _netplan_g_string_free_to_file(s, rootdir, path, ".netdev"); umask(orig_umask); } STATIC void write_route(NetplanIPRoute* r, GString* s) { const char *to; g_string_append_printf(s, "\n[Route]\n"); if (g_strcmp0(r->to, "default") == 0) to = get_global_network(r->family); else to = r->to; g_string_append_printf(s, "Destination=%s\n", to); if (r->via) g_string_append_printf(s, "Gateway=%s\n", r->via); if (r->from) g_string_append_printf(s, "PreferredSource=%s\n", r->from); if (g_strcmp0(r->scope, "global") != 0) g_string_append_printf(s, "Scope=%s\n", r->scope); if (g_strcmp0(r->type, "unicast") != 0) g_string_append_printf(s, "Type=%s\n", r->type); if (r->onlink) g_string_append_printf(s, "GatewayOnLink=true\n"); if (r->metric != NETPLAN_METRIC_UNSPEC) g_string_append_printf(s, "Metric=%u\n", r->metric); if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC) g_string_append_printf(s, "Table=%d\n", r->table); if (r->mtubytes != NETPLAN_MTU_UNSPEC) g_string_append_printf(s, "MTUBytes=%u\n", r->mtubytes); if (r->congestion_window != NETPLAN_CONGESTION_WINDOW_UNSPEC) g_string_append_printf(s, "InitialCongestionWindow=%u\n", r->congestion_window); if (r->advertised_receive_window != NETPLAN_ADVERTISED_RECEIVE_WINDOW_UNSPEC) g_string_append_printf(s, "InitialAdvertisedReceiveWindow=%u\n", r->advertised_receive_window); } STATIC void write_ip_rule(NetplanIPRule* r, GString* s) { g_string_append_printf(s, "\n[RoutingPolicyRule]\n"); if (r->from) g_string_append_printf(s, "From=%s\n", r->from); if (r->to) g_string_append_printf(s, "To=%s\n", r->to); if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC) g_string_append_printf(s, "Table=%d\n", r->table); if (r->priority != NETPLAN_IP_RULE_PRIO_UNSPEC) g_string_append_printf(s, "Priority=%d\n", r->priority); if (r->fwmark != NETPLAN_IP_RULE_FW_MARK_UNSPEC) g_string_append_printf(s, "FirewallMark=%d\n", r->fwmark); if (r->tos != NETPLAN_IP_RULE_TOS_UNSPEC) g_string_append_printf(s, "TypeOfService=%d\n", r->tos); } STATIC void write_addr_option(NetplanAddressOptions* o, GString* s) { g_string_append_printf(s, "\n[Address]\n"); g_assert(o->address); g_string_append_printf(s, "Address=%s\n", o->address); if (o->lifetime) g_string_append_printf(s, "PreferredLifetime=%s\n", o->lifetime); if (o->label) g_string_append_printf(s, "Label=%s\n", o->label); } #define DHCP_OVERRIDES_ERROR \ "ERROR: %s: networkd requires that %s has the same value in both " \ "dhcp4_overrides and dhcp6_overrides\n" STATIC gboolean combine_dhcp_overrides(const NetplanNetDefinition* def, NetplanDHCPOverrides* combined_dhcp_overrides, GError** error) { /* if only one of dhcp4 or dhcp6 is enabled, those overrides are used */ if (def->dhcp4 && !def->dhcp6) { *combined_dhcp_overrides = def->dhcp4_overrides; } else if (!def->dhcp4 && def->dhcp6) { *combined_dhcp_overrides = def->dhcp6_overrides; } else { /* networkd doesn't support separately configuring dhcp4 and dhcp6, so * we enforce that they are the same. */ if (def->dhcp4_overrides.use_dns != def->dhcp6_overrides.use_dns) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, DHCP_OVERRIDES_ERROR, def->id, "use-dns"); return FALSE; } if (g_strcmp0(def->dhcp4_overrides.use_domains, def->dhcp6_overrides.use_domains) != 0){ g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, DHCP_OVERRIDES_ERROR, def->id, "use-domains"); return FALSE; } if (def->dhcp4_overrides.use_ntp != def->dhcp6_overrides.use_ntp) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, DHCP_OVERRIDES_ERROR, def->id, "use-ntp"); return FALSE; } if (def->dhcp4_overrides.send_hostname != def->dhcp6_overrides.send_hostname) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, DHCP_OVERRIDES_ERROR, def->id, "send-hostname"); return FALSE; } if (def->dhcp4_overrides.use_hostname != def->dhcp6_overrides.use_hostname) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, DHCP_OVERRIDES_ERROR, def->id, "use-hostname"); return FALSE; } if (def->dhcp4_overrides.use_mtu != def->dhcp6_overrides.use_mtu) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, DHCP_OVERRIDES_ERROR, def->id, "use-mtu"); return FALSE; } if (g_strcmp0(def->dhcp4_overrides.hostname, def->dhcp6_overrides.hostname) != 0) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, DHCP_OVERRIDES_ERROR, def->id, "hostname"); return FALSE; } if (def->dhcp4_overrides.metric != def->dhcp6_overrides.metric) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, DHCP_OVERRIDES_ERROR, def->id, "route-metric"); return FALSE; } if (def->dhcp4_overrides.use_routes != def->dhcp6_overrides.use_routes) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, DHCP_OVERRIDES_ERROR, def->id, "use-routes"); return FALSE; } /* Just use dhcp4_overrides now, since we know they are the same. */ *combined_dhcp_overrides = def->dhcp4_overrides; } return TRUE; } /** * Write the needed networkd .network configuration for the selected netplan definition. */ gboolean _netplan_netdef_write_network_file( const NetplanState* np_state, const NetplanNetDefinition* def, const char *rootdir, const char* path, gboolean* has_been_written, GError** error) { g_autoptr(GString) network = NULL; g_autoptr(GString) link = NULL; GString* s = NULL; mode_t orig_umask; gboolean is_optional = def->optional; SET_OPT_OUT_PTR(has_been_written, FALSE); if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) { g_debug("%s is defined as a hardware SR-IOV filtered VLAN, postponing creation", def->id); return TRUE; } /* Prepare the [Link] section of the .network file. */ link = g_string_sized_new(200); /* Prepare the [Network] section */ network = g_string_sized_new(200); /* The ActivationPolicy setting is available in systemd v248+ */ if (def->activation_mode) { const char* mode; if (g_strcmp0(def->activation_mode, "manual") == 0) mode = "manual"; else /* "off" */ mode = "always-down"; g_string_append_printf(link, "ActivationPolicy=%s\n", mode); /* When activation-mode is used we default to being optional. * Otherwise systemd might wait indefinitely for the interface to * become online. */ is_optional = TRUE; } if (is_optional || def->optional_addresses) { if (is_optional) { g_string_append(link, "RequiredForOnline=no\n"); } for (unsigned i = 0; NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name != NULL; ++i) { if (def->optional_addresses & NETPLAN_OPTIONAL_ADDRESS_TYPES[i].flag) { g_string_append_printf(link, "OptionalAddresses=%s\n", NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name); } } } if (def->mtubytes) g_string_append_printf(link, "MTUBytes=%u\n", def->mtubytes); if (def->set_mac && _is_valid_macaddress(def->set_mac)) g_string_append_printf(link, "MACAddress=%s\n", def->set_mac); if (def->emit_lldp) g_string_append(network, "EmitLLDP=true\n"); if (def->dhcp4 && def->dhcp6) g_string_append(network, "DHCP=yes\n"); else if (def->dhcp4) g_string_append(network, "DHCP=ipv4\n"); else if (def->dhcp6) g_string_append(network, "DHCP=ipv6\n"); /* Set link local addressing -- this does not apply to bond and bridge * member interfaces, which always get it disabled. */ if (!def->bond && !def->bridge && (def->linklocal.ipv4 || def->linklocal.ipv6)) { if (def->linklocal.ipv4 && def->linklocal.ipv6) g_string_append(network, "LinkLocalAddressing=yes\n"); else if (def->linklocal.ipv4) g_string_append(network, "LinkLocalAddressing=ipv4\n"); else if (def->linklocal.ipv6) g_string_append(network, "LinkLocalAddressing=ipv6\n"); } else { g_string_append(network, "LinkLocalAddressing=no\n"); } if (def->ip4_addresses) for (unsigned i = 0; i < def->ip4_addresses->len; ++i) g_string_append_printf(network, "Address=%s\n", g_array_index(def->ip4_addresses, char*, i)); if (def->ip6_addresses) for (unsigned i = 0; i < def->ip6_addresses->len; ++i) g_string_append_printf(network, "Address=%s\n", g_array_index(def->ip6_addresses, char*, i)); if (def->ip6_addr_gen_token) { g_string_append_printf(network, "IPv6Token=static:%s\n", def->ip6_addr_gen_token); } else if (def->ip6_addr_gen_mode > NETPLAN_ADDRGEN_EUI64) { /* EUI-64 mode is enabled by default, if no IPv6Token= is specified */ /* TODO: Enable stable-privacy mode for networkd, once PR#16618 has been released: * https://github.com/systemd/systemd/pull/16618 */ g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: ipv6-address-generation mode is not supported by networkd\n", def->id); return FALSE; } if (def->accept_ra == NETPLAN_RA_MODE_ENABLED) g_string_append_printf(network, "IPv6AcceptRA=yes\n"); else if (def->accept_ra == NETPLAN_RA_MODE_DISABLED) g_string_append_printf(network, "IPv6AcceptRA=no\n"); if (def->ip6_privacy) g_string_append(network, "IPv6PrivacyExtensions=yes\n"); if (def->gateway4) g_string_append_printf(network, "Gateway=%s\n", def->gateway4); if (def->gateway6) g_string_append_printf(network, "Gateway=%s\n", def->gateway6); if (def->ip4_nameservers) for (unsigned i = 0; i < def->ip4_nameservers->len; ++i) g_string_append_printf(network, "DNS=%s\n", g_array_index(def->ip4_nameservers, char*, i)); if (def->ip6_nameservers) for (unsigned i = 0; i < def->ip6_nameservers->len; ++i) g_string_append_printf(network, "DNS=%s\n", g_array_index(def->ip6_nameservers, char*, i)); if (def->search_domains) { g_string_append_printf(network, "Domains=%s", g_array_index(def->search_domains, char*, 0)); for (unsigned i = 1; i < def->search_domains->len; ++i) g_string_append_printf(network, " %s", g_array_index(def->search_domains, char*, i)); g_string_append(network, "\n"); } if (def->ipv6_mtubytes) { g_string_append_printf(network, "IPv6MTUBytes=%d\n", def->ipv6_mtubytes); } if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL || def->ignore_carrier) g_string_append(network, "ConfigureWithoutCarrier=yes\n"); if (def->critical) g_string_append_printf(network, "KeepConfiguration=true\n"); if (def->bridge && def->backend != NETPLAN_BACKEND_OVS) { g_string_append_printf(network, "Bridge=%s\n", def->bridge); if ( def->bridge_params.path_cost || def->bridge_params.port_priority || def->bridge_hairpin != NETPLAN_TRISTATE_UNSET || def->bridge_learning != NETPLAN_TRISTATE_UNSET || def->bridge_neigh_suppress != NETPLAN_TRISTATE_UNSET) g_string_append_printf(network, "\n[Bridge]\n"); if (def->bridge_params.path_cost) g_string_append_printf(network, "Cost=%u\n", def->bridge_params.path_cost); if (def->bridge_params.port_priority) g_string_append_printf(network, "Priority=%u\n", def->bridge_params.port_priority); if (def->bridge_hairpin != NETPLAN_TRISTATE_UNSET) g_string_append_printf(network, "HairPin=%s\n", def->bridge_hairpin ? "true" : "false"); if (def->bridge_learning != NETPLAN_TRISTATE_UNSET) g_string_append_printf(network, "Learning=%s\n", def->bridge_learning ? "true" : "false"); if (def->bridge_neigh_suppress != NETPLAN_TRISTATE_UNSET) g_string_append_printf(network, "NeighborSuppression=%s\n", def->bridge_neigh_suppress ? "true" : "false"); } if (def->bond && def->backend != NETPLAN_BACKEND_OVS) { g_string_append_printf(network, "Bond=%s\n", def->bond); if (def->bond_params.primary_member) g_string_append_printf(network, "PrimarySlave=true\n"); /* wokeignore:rule=slave */ } if (def->has_vlans && def->backend != NETPLAN_BACKEND_OVS) { /* iterate over all netdefs to find VLANs attached to us */ GList *l = np_state->netdefs_ordered; const NetplanNetDefinition* nd; for (; l != NULL; l = l->next) { nd = l->data; if (nd->vlan_link == def && !nd->sriov_vlan_filter) g_string_append_printf(network, "VLAN=%s\n", nd->id); } } /* VRF linkage */ if (def->vrf_link) g_string_append_printf(network, "VRF=%s\n", def->vrf_link->id); /* VXLAN options */ if (def->has_vxlans) { /* iterate over all netdefs to find VXLANs attached to us */ GList *l = np_state->netdefs_ordered; const NetplanNetDefinition* nd; for (; l != NULL; l = l->next) { nd = l->data; if (nd->vxlan && nd->vxlan->link == def && nd->type == NETPLAN_DEF_TYPE_TUNNEL && nd->tunnel.mode == NETPLAN_TUNNEL_MODE_VXLAN) g_string_append_printf(network, "VXLAN=%s\n", nd->id); } } if (def->routes != NULL) { for (unsigned i = 0; i < def->routes->len; ++i) { NetplanIPRoute* cur_route = g_array_index (def->routes, NetplanIPRoute*, i); write_route(cur_route, network); } } if (def->ip_rules != NULL) { for (unsigned i = 0; i < def->ip_rules->len; ++i) { NetplanIPRule* cur_rule = g_array_index (def->ip_rules, NetplanIPRule*, i); write_ip_rule(cur_rule, network); } } if (def->address_options) { for (unsigned i = 0; i < def->address_options->len; ++i) { NetplanAddressOptions* opts = g_array_index(def->address_options, NetplanAddressOptions*, i); write_addr_option(opts, network); } } if (def->dhcp4 || def->dhcp6) { /* NetworkManager compatible route metrics */ g_string_append(network, "\n[DHCP]\n"); } if (def->dhcp4 || def->dhcp6) { if (def->dhcp_identifier) g_string_append_printf(network, "ClientIdentifier=%s\n", def->dhcp_identifier); NetplanDHCPOverrides combined_dhcp_overrides; if (!combine_dhcp_overrides(def, &combined_dhcp_overrides, error)) return FALSE; if (combined_dhcp_overrides.metric == NETPLAN_METRIC_UNSPEC) { g_string_append_printf(network, "RouteMetric=%i\n", (def->type == NETPLAN_DEF_TYPE_WIFI ? 600 : 100)); } else { g_string_append_printf(network, "RouteMetric=%u\n", combined_dhcp_overrides.metric); } /* Only set MTU from DHCP if use-mtu dhcp-override is not false. */ if (!combined_dhcp_overrides.use_mtu) { /* isc-dhcp dhclient compatible UseMTU, networkd default is to * not accept MTU, which breaks clouds */ g_string_append_printf(network, "UseMTU=false\n"); } else { g_string_append_printf(network, "UseMTU=true\n"); } /* Only write DHCP options that differ from the networkd default. */ if (!combined_dhcp_overrides.use_routes) g_string_append_printf(network, "UseRoutes=false\n"); if (!combined_dhcp_overrides.use_dns) g_string_append_printf(network, "UseDNS=false\n"); if (combined_dhcp_overrides.use_domains) g_string_append_printf(network, "UseDomains=%s\n", combined_dhcp_overrides.use_domains); if (!combined_dhcp_overrides.use_ntp) g_string_append_printf(network, "UseNTP=false\n"); if (!combined_dhcp_overrides.send_hostname) g_string_append_printf(network, "SendHostname=false\n"); if (!combined_dhcp_overrides.use_hostname) g_string_append_printf(network, "UseHostname=false\n"); if (combined_dhcp_overrides.hostname) g_string_append_printf(network, "Hostname=%s\n", combined_dhcp_overrides.hostname); } /* IP-over-InfiniBand, IPoIB */ if (def->ib_mode != NETPLAN_IB_MODE_KERNEL) { g_string_append_printf(network, "\n[IPoIB]\nMode=%s\n", netplan_infiniband_mode_name(def->ib_mode)); } if (network->len > 0 || link->len > 0) { s = g_string_sized_new(200); append_match_section(def, s, TRUE); if (link->len > 0) g_string_append_printf(s, "\n[Link]\n%s", link->str); if (network->len > 0) g_string_append_printf(s, "\n[Network]\n%s", network->str); /* these do not contain secrets and need to be readable by * systemd-networkd - LP: #1736965 */ orig_umask = umask(022); _netplan_g_string_free_to_file(s, rootdir, path, ".network"); umask(orig_umask); } SET_OPT_OUT_PTR(has_been_written, TRUE); return TRUE; } STATIC void write_rules_file(const NetplanNetDefinition* def, const char* rootdir) { GString* s = NULL; g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL); mode_t orig_umask; /* do we need to write a .rules file? * It's only required for reliably setting the name of a physical device * until systemd issue #9006 is resolved. */ if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL) return; /* Matching by name does not work. * * As far as I can tell, if you match by the name coming out of * initrd, systemd complains that a link file is matching on a * renamed name. If you match by the unstable kernel name, the * device no longer has that name when udevd reads the file, so * the rule doesn't fire. So only support mac and driver. */ if (!def->set_name || (!def->match.mac && !def->match.driver)) return; /* build file contents */ s = g_string_sized_new(200); g_string_append(s, "SUBSYSTEM==\"net\", ACTION==\"add\", "); if (def->match.driver) { g_string_append_printf(s,"DRIVERS==\"%s\", ", def->match.driver); } else { g_string_append(s, "DRIVERS==\"?*\", "); } if (def->match.mac) g_string_append_printf(s, "ATTR{address}==\"%s\", ", def->match.mac); g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name); orig_umask = umask(022); _netplan_g_string_free_to_file(s, rootdir, path, NULL); umask(orig_umask); } STATIC gboolean append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, const char* id, GError** error) { switch (auth->key_management) { case NETPLAN_AUTH_KEY_MANAGEMENT_NONE: g_string_append(s, " key_mgmt=NONE\n"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK: if (auth->pmf_mode == NETPLAN_AUTH_PMF_MODE_OPTIONAL) /* Case where the user only provided the password. * We enable support for WPA2 and WPA3 personal. */ g_string_append(s, " key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE\n"); else g_string_append(s, " key_mgmt=WPA-PSK\n"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP: g_string_append(s, " key_mgmt=WPA-EAP\n"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSHA256: g_string_append(s, " key_mgmt=WPA-EAP WPA-EAP-SHA256\n"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSUITE_B_192: g_string_append(s, " key_mgmt=WPA-EAP-SUITE-B-192\n"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_SAE: g_string_append(s, " key_mgmt=SAE\n"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_8021X: g_string_append(s, " key_mgmt=IEEE8021X\n"); break; default: break; // LCOV_EXCL_LINE } switch (auth->eap_method) { case NETPLAN_AUTH_EAP_NONE: break; case NETPLAN_AUTH_EAP_TLS: g_string_append(s, " eap=TLS\n"); break; case NETPLAN_AUTH_EAP_PEAP: g_string_append(s, " eap=PEAP\n"); break; case NETPLAN_AUTH_EAP_TTLS: g_string_append(s, " eap=TTLS\n"); break; case NETPLAN_AUTH_EAP_LEAP: g_string_append(s, " eap=LEAP\n"); break; case NETPLAN_AUTH_EAP_PWD: g_string_append(s, " eap=PWD\n"); break; default: break; // LCOV_EXCL_LINE } switch (auth->pmf_mode) { case NETPLAN_AUTH_PMF_MODE_NONE: case NETPLAN_AUTH_PMF_MODE_DISABLED: break; case NETPLAN_AUTH_PMF_MODE_OPTIONAL: g_string_append(s, " ieee80211w=1\n"); break; case NETPLAN_AUTH_PMF_MODE_REQUIRED: g_string_append(s, " ieee80211w=2\n"); break; } if (auth->identity) { g_string_append_printf(s, " identity=\"%s\"\n", auth->identity); } if (auth->anonymous_identity) { g_string_append_printf(s, " anonymous_identity=\"%s\"\n", auth->anonymous_identity); } char* psk = NULL; if (auth->psk) psk = auth->psk; else if (auth->password && _is_auth_key_management_psk(auth)) psk = auth->password; if (psk) { size_t len = strlen(psk); if (len == 64) { /* must be a hex-digit key representation */ for (unsigned i = 0; i < 64; ++i) if (!isxdigit(psk[i])) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: PSK length of 64 is only supported for hex-digit representation\n", id); return FALSE; } /* this is required to be unquoted */ g_string_append_printf(s, " psk=%s\n", psk); } else if (len < 8 || len > 63) { /* per wpa_supplicant spec, passphrase needs to be between 8 and 63 characters */ g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "ERROR: %s: ASCII passphrase must be between 8 and 63 characters (inclusive)\n", id); return FALSE; } else { g_string_append_printf(s, " psk=\"%s\"\n", psk); } } if (auth->password && (!_is_auth_key_management_psk(auth) || auth->eap_method != NETPLAN_AUTH_EAP_NONE)) { if (strncmp(auth->password, "hash:", 5) == 0) { g_string_append_printf(s, " password=%s\n", auth->password); } else { g_string_append_printf(s, " password=\"%s\"\n", auth->password); } } if (auth->ca_certificate) { g_string_append_printf(s, " ca_cert=\"%s\"\n", auth->ca_certificate); } if (auth->client_certificate) { g_string_append_printf(s, " client_cert=\"%s\"\n", auth->client_certificate); } if (auth->client_key) { g_string_append_printf(s, " private_key=\"%s\"\n", auth->client_key); } if (auth->client_key_password) { g_string_append_printf(s, " private_key_passwd=\"%s\"\n", auth->client_key_password); } if (auth->phase2_auth) { g_string_append_printf(s, " phase2=\"auth=%s\"\n", auth->phase2_auth); } return TRUE; } /* netplan-feature: generated-supplicant */ STATIC void write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir) { g_autofree gchar *stdouth = NULL; mode_t orig_umask; stdouth = systemd_escape(def->id); GString* s = g_string_new("[Unit]\n"); g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", stdouth, ".service", NULL); g_string_append_printf(s, "Description=WPA supplicant for netplan %s\n", stdouth); g_string_append(s, "DefaultDependencies=no\n"); g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", stdouth); g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", stdouth); g_string_append(s, "Before=network.target\nWants=network.target\n\n"); g_string_append(s, "[Service]\nType=simple\n"); g_string_append_printf(s, "ExecStart=/sbin/wpa_supplicant -c /run/netplan/wpa-%s.conf -i%s", stdouth, stdouth); if (def->type != NETPLAN_DEF_TYPE_WIFI) { g_string_append(s, " -Dwired\n"); } else { g_string_append(s, " -Dnl80211,wext\n"); } orig_umask = umask(022); _netplan_g_string_free_to_file(s, rootdir, path, NULL); umask(orig_umask); } STATIC gboolean write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** error) { GHashTableIter iter; GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n"); g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL); mode_t orig_umask; g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path); if (def->type == NETPLAN_DEF_TYPE_WIFI) { if (def->wowlan && def->wowlan > NETPLAN_WIFI_WOWLAN_DEFAULT) { g_string_append(s, "wowlan_triggers="); if (!append_wifi_wowlan_flags(def->wowlan, s, error)) { g_string_free(s, TRUE); return FALSE; } } /* available as of wpa_supplicant version 0.6.7 */ if (def->regulatory_domain) { g_string_append_printf(s, "country=%s\n", def->regulatory_domain); } NetplanWifiAccessPoint* ap; g_hash_table_iter_init(&iter, def->access_points); while (g_hash_table_iter_next(&iter, NULL, (gpointer) &ap)) { gchar* freq_config_str = ap->mode == NETPLAN_WIFI_MODE_ADHOC ? "frequency" : "freq_list"; g_string_append_printf(s, "network={\n ssid=\"%s\"\n", ap->ssid); if (ap->bssid) { g_string_append_printf(s, " bssid=%s\n", ap->bssid); } if (ap->hidden) { g_string_append(s, " scan_ssid=1\n"); } if (ap->band == NETPLAN_WIFI_BAND_24) { // initialize 2.4GHz frequency hashtable if(!wifi_frequency_24) wifi_get_freq24(1); if (ap->channel) { g_string_append_printf(s, " %s=%d\n", freq_config_str, wifi_get_freq24(ap->channel)); } else if (ap->mode != NETPLAN_WIFI_MODE_ADHOC) { g_string_append_printf(s, " freq_list="); g_hash_table_foreach(wifi_frequency_24, wifi_append_freq, s); // overwrite last whitespace with newline s = g_string_overwrite(s, s->len-1, "\n"); } } else if (ap->band == NETPLAN_WIFI_BAND_5) { // initialize 5GHz frequency hashtable if(!wifi_frequency_5) wifi_get_freq5(7); if (ap->channel) { g_string_append_printf(s, " %s=%d\n", freq_config_str, wifi_get_freq5(ap->channel)); } else if (ap->mode != NETPLAN_WIFI_MODE_ADHOC) { g_string_append_printf(s, " freq_list="); g_hash_table_foreach(wifi_frequency_5, wifi_append_freq, s); // overwrite last whitespace with newline s = g_string_overwrite(s, s->len-1, "\n"); } } switch (ap->mode) { case NETPLAN_WIFI_MODE_INFRASTRUCTURE: /* default in wpasupplicant */ break; case NETPLAN_WIFI_MODE_ADHOC: g_string_append(s, " mode=1\n"); break; default: g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: %s: networkd does not support this wifi mode\n", def->id, ap->ssid); g_string_free(s, TRUE); return FALSE; } /* wifi auth trumps netdef auth */ if (ap->has_auth) { if (!append_wpa_auth_conf(s, &ap->auth, ap->ssid, error)) { g_string_free(s, TRUE); return FALSE; } } else { g_string_append(s, " key_mgmt=NONE\n"); } g_string_append(s, "}\n"); } } else { /* wired 802.1x auth or similar */ g_string_append(s, "network={\n"); if (!append_wpa_auth_conf(s, &def->auth, def->id, error)) { g_string_free(s, TRUE); return FALSE; } g_string_append(s, "}\n"); } /* use tight permissions as this contains secrets */ orig_umask = umask(077); _netplan_g_string_free_to_file(s, rootdir, path, NULL); umask(orig_umask); return TRUE; } /** * Generate networkd configuration in @rootdir/run/systemd/network/ from the * parsed #netdefs. * @rootdir: If not %NULL, generate configuration in this root directory * (useful for testing). * @has_been_written: TRUE if @def applies to networkd, FALSE otherwise. * Returns: FALSE on error. */ gboolean _netplan_netdef_write_networkd( const NetplanState* np_state, const NetplanNetDefinition* def, const char *rootdir, gboolean* has_been_written, GError** error) { /* TODO: make use of netplan_netdef_get_output_filename() */ g_autofree char* path_base = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL); SET_OPT_OUT_PTR(has_been_written, FALSE); /* We want this for all backends when renaming, as *.link and *.rules files are * evaluated by udev, not networkd itself or NetworkManager. The regulatory * domain applies to all backends, too. */ write_link_file(def, rootdir, path_base); write_rules_file(def, rootdir); if (def->regulatory_domain) write_regdom(def, rootdir, NULL); /* overwrites global regdom */ if (def->backend != NETPLAN_BACKEND_NETWORKD) { g_debug("networkd: definition %s is not for us (backend %i)", def->id, def->backend); return TRUE; } if (def->type == NETPLAN_DEF_TYPE_MODEM) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: networkd backend does not support GSM/CDMA modem configuration\n", def->id); return FALSE; } if (def->type == NETPLAN_DEF_TYPE_WIFI || def->has_auth) { g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-", def->id, ".service", NULL); g_autofree char* slink = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", def->id, ".service", NULL); if (def->type == NETPLAN_DEF_TYPE_WIFI && def->has_match) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: networkd backend does not support wifi with match:, only by interface name\n", def->id); return FALSE; } g_debug("Creating wpa_supplicant config"); if (!write_wpa_conf(def, rootdir, error)) return FALSE; g_debug("Creating wpa_supplicant unit %s", slink); write_wpa_unit(def, rootdir); g_debug("Creating wpa_supplicant service enablement link %s", link); _netplan_safe_mkdir_p_dir(link); if (symlink(slink, link) < 0 && errno != EEXIST) { // LCOV_EXCL_START g_set_error(error, NETPLAN_FILE_ERROR, errno, "failed to create enablement symlink: %m\n"); return FALSE; // LCOV_EXCL_STOP } } if (def->set_mac && !_is_valid_macaddress(def->set_mac) && !_is_macaddress_special_nd_option(def->set_mac)) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: networkd backend does not support the MAC address option '%s'\n", def->id, def->set_mac); return FALSE; } if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL) write_netdev_file(def, rootdir, path_base); if (!_netplan_netdef_write_network_file(np_state, def, rootdir, path_base, has_been_written, error)) return FALSE; SET_OPT_OUT_PTR(has_been_written, TRUE); return TRUE; } /** * Clean up all generated configurations in @rootdir from previous runs. */ void _netplan_networkd_cleanup(const char* rootdir) { _netplan_unlink_glob(rootdir, "/run/systemd/network/10-netplan-*"); _netplan_unlink_glob(rootdir, "/run/netplan/wpa-*.conf"); _netplan_unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-*.service"); _netplan_unlink_glob(rootdir, "/run/systemd/system/netplan-wpa-*.service"); _netplan_unlink_glob(rootdir, "/run/udev/rules.d/99-netplan-*"); _netplan_unlink_glob(rootdir, "/run/systemd/system/network.target.wants/netplan-regdom.service"); _netplan_unlink_glob(rootdir, "/run/systemd/system/netplan-regdom.service"); /* Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an * upgraded system, we need to make sure to clean those up. */ _netplan_unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa@*.service"); } netplan-1.0/src/networkd.h000066400000000000000000000024111457004145200156100ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * Author: Martin Pitt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "netplan.h" #include NETPLAN_INTERNAL gboolean _netplan_netdef_write_networkd( const NetplanState* np_state, const NetplanNetDefinition* def, const char *rootdir, gboolean* has_been_written, GError** error); NETPLAN_INTERNAL gboolean _netplan_netdef_write_network_file( const NetplanState* np_state, const NetplanNetDefinition* def, const char *rootdir, const char* path, gboolean* has_been_written, GError** error); NETPLAN_INTERNAL void _netplan_networkd_cleanup(const char* rootdir); netplan-1.0/src/nm.c000066400000000000000000001465661457004145200144040ustar00rootroot00000000000000/* * Copyright (C) 2016-2021 Canonical, Ltd. * Author: Martin Pitt * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include "names.h" #include "netplan.h" #include "nm.h" #include "parse.h" #include "parse-nm.h" #include "util.h" #include "util-internal.h" #include "validation.h" /** * Infer if this is a modem netdef of type GSM. * This is done by checking for certain modem_params, which are only * applicable to GSM connections. */ STATIC gboolean modem_is_gsm(const NetplanNetDefinition* def) { if ( def->modem_params.apn || def->modem_params.auto_config || def->modem_params.device_id || def->modem_params.network_id || def->modem_params.pin || def->modem_params.sim_id || def->modem_params.sim_operator_id) return TRUE; return FALSE; } /** * Return NM "type=" string. */ STATIC char* type_str(const NetplanNetDefinition* def) { const NetplanDefType type = def->type; switch (type) { case NETPLAN_DEF_TYPE_ETHERNET: /* 20-byte IPoIB MAC + colons */ if (def->ib_mode || (def->match.mac && strlen(def->match.mac) == 59)) return "infiniband"; else return "ethernet"; case NETPLAN_DEF_TYPE_MODEM: if (modem_is_gsm(def)) return "gsm"; else return "cdma"; case NETPLAN_DEF_TYPE_WIFI: return "wifi"; case NETPLAN_DEF_TYPE_BRIDGE: return "bridge"; case NETPLAN_DEF_TYPE_BOND: return "bond"; case NETPLAN_DEF_TYPE_VLAN: return "vlan"; case NETPLAN_DEF_TYPE_VRF: return "vrf"; case NETPLAN_DEF_TYPE_DUMMY: /* wokeignore:rule=dummy */ return "dummy"; /* wokeignore:rule=dummy */ case NETPLAN_DEF_TYPE_VETH: return "veth"; case NETPLAN_DEF_TYPE_TUNNEL: if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) return "wireguard"; else if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_VXLAN) return "vxlan"; return "ip-tunnel"; case NETPLAN_DEF_TYPE_NM: /* needs to be overriden by passthrough "connection.type" setting */ g_assert(def->backend_settings.passthrough); GData *passthrough = def->backend_settings.passthrough; return g_datalist_get_data(&passthrough, "connection.type"); // LCOV_EXCL_START default: g_assert_not_reached(); // LCOV_EXCL_STOP } } /** * Return NM wifi "mode=" string. */ STATIC char* wifi_mode_str(const NetplanWifiMode mode) { switch (mode) { case NETPLAN_WIFI_MODE_INFRASTRUCTURE: return "infrastructure"; case NETPLAN_WIFI_MODE_ADHOC: return "adhoc"; case NETPLAN_WIFI_MODE_AP: return "ap"; // LCOV_EXCL_START default: g_assert_not_reached(); // LCOV_EXCL_STOP } } /** * Return NM wifi "band=" string. */ STATIC char* wifi_band_str(const NetplanWifiBand band) { switch (band) { case NETPLAN_WIFI_BAND_5: return "a"; case NETPLAN_WIFI_BAND_24: return "bg"; // LCOV_EXCL_START default: g_assert_not_reached(); // LCOV_EXCL_STOP } } /** * Return NM addr-gen-mode string. */ STATIC char* addr_gen_mode_str(const NetplanAddrGenMode mode) { switch (mode) { case NETPLAN_ADDRGEN_EUI64: return "0"; case NETPLAN_ADDRGEN_STABLEPRIVACY: return "1"; // LCOV_EXCL_START default: g_assert_not_reached(); // LCOV_EXCL_STOP } } STATIC void write_search_domains(const NetplanNetDefinition* def, const char* group, GKeyFile *kf) { if (def->search_domains) { const gchar* list[def->search_domains->len]; for (unsigned i = 0; i < def->search_domains->len; ++i) list[i] = g_array_index(def->search_domains, char*, i); g_key_file_set_string_list(kf, group, "dns-search", list, def->search_domains->len); } } STATIC gboolean write_routes_nm(const NetplanNetDefinition* def, GKeyFile *kf, gint family, GError** error) { const gchar* group = NULL; gchar* tmp_key = NULL; GString* tmp_val = NULL; if (family == AF_INET) group = "ipv4"; else if (family == AF_INET6) group = "ipv6"; g_assert(group != NULL); if (def->routes != NULL) { for (unsigned i = 0, j = 1; i < def->routes->len; ++i) { const NetplanIPRoute *cur_route = g_array_index(def->routes, NetplanIPRoute*, i); const char *destination; if (cur_route->family != family) continue; if (g_strcmp0(cur_route->to, "default") == 0) destination = get_global_network(family); else destination = cur_route->to; if (cur_route->type && g_ascii_strcasecmp(cur_route->type, "unicast") != 0) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: NetworkManager only supports unicast routes\n", def->id); return FALSE; } /* For IPv6 addresses, kernel and NetworkManager don't support a scope. * For IPv4 addresses, NetworkManager determines the scope of addresses on its own * ("link"/"host" for addresses without gateway, "global" for addresses with next-hop). * No gateway is represented as missing, empty or unspecified address in keyfile. */ gboolean is_global = (g_strcmp0(cur_route->scope, "global") == 0); tmp_key = g_strdup_printf("route%d", j); tmp_val = g_string_new(destination); if (cur_route->metric != NETPLAN_METRIC_UNSPEC) g_string_append_printf(tmp_val, ",%s,%u", is_global ? cur_route->via : "", cur_route->metric); else if (is_global) // no metric, but global gateway g_string_append_printf(tmp_val, ",%s", cur_route->via); g_key_file_set_string(kf, group, tmp_key, tmp_val->str); g_free(tmp_key); g_string_free(tmp_val, TRUE); if ( cur_route->onlink || cur_route->advertised_receive_window || cur_route->congestion_window || cur_route->mtubytes || cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC || cur_route->from) { tmp_key = g_strdup_printf("route%d_options", j); tmp_val = g_string_new(NULL); if (cur_route->onlink) { /* onlink for IPv6 addresses is only supported since nm-1.18.0. */ g_string_append_printf(tmp_val, "onlink=true,"); } if (cur_route->advertised_receive_window != NETPLAN_ADVERTISED_RECEIVE_WINDOW_UNSPEC) g_string_append_printf(tmp_val, "initrwnd=%u,", cur_route->advertised_receive_window); if (cur_route->congestion_window != NETPLAN_CONGESTION_WINDOW_UNSPEC) g_string_append_printf(tmp_val, "initcwnd=%u,", cur_route->congestion_window); if (cur_route->mtubytes != NETPLAN_MTU_UNSPEC) g_string_append_printf(tmp_val, "mtu=%u,", cur_route->mtubytes); if (cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC) g_string_append_printf(tmp_val, "table=%u,", cur_route->table); if (cur_route->from) g_string_append_printf(tmp_val, "src=%s,", cur_route->from); tmp_val->str[tmp_val->len - 1] = '\0'; //remove trailing comma g_key_file_set_string(kf, group, tmp_key, tmp_val->str); g_free(tmp_key); g_string_free(tmp_val, TRUE); } j++; } } return TRUE; } STATIC void write_nm_bond_parameters(const NetplanNetDefinition* def, GKeyFile *kf) { GString* tmp_val = NULL; if (def->bond_params.mode) g_key_file_set_string(kf, "bond", "mode", def->bond_params.mode); if (def->bond_params.lacp_rate) g_key_file_set_string(kf, "bond", "lacp_rate", def->bond_params.lacp_rate); if (def->bond_params.monitor_interval) g_key_file_set_string(kf, "bond", "miimon", def->bond_params.monitor_interval); if (def->bond_params.min_links) g_key_file_set_integer(kf, "bond", "min_links", def->bond_params.min_links); if (def->bond_params.transmit_hash_policy) g_key_file_set_string(kf, "bond", "xmit_hash_policy", def->bond_params.transmit_hash_policy); if (def->bond_params.selection_logic) g_key_file_set_string(kf, "bond", "ad_select", def->bond_params.selection_logic); if (def->bond_params.all_members_active) g_key_file_set_integer(kf, "bond", "all_slaves_active", def->bond_params.all_members_active); /* wokeignore:rule=slave */ if (def->bond_params.arp_interval) g_key_file_set_string(kf, "bond", "arp_interval", def->bond_params.arp_interval); if (def->bond_params.arp_ip_targets) { tmp_val = g_string_new(NULL); for (unsigned i = 0; i < def->bond_params.arp_ip_targets->len; ++i) { if (i > 0) g_string_append_printf(tmp_val, ","); g_string_append_printf(tmp_val, "%s", g_array_index(def->bond_params.arp_ip_targets, char*, i)); } g_key_file_set_string(kf, "bond", "arp_ip_target", tmp_val->str); g_string_free(tmp_val, TRUE); } if (def->bond_params.arp_validate) g_key_file_set_string(kf, "bond", "arp_validate", def->bond_params.arp_validate); if (def->bond_params.arp_all_targets) g_key_file_set_string(kf, "bond", "arp_all_targets", def->bond_params.arp_all_targets); if (def->bond_params.up_delay) g_key_file_set_string(kf, "bond", "updelay", def->bond_params.up_delay); if (def->bond_params.down_delay) g_key_file_set_string(kf, "bond", "downdelay", def->bond_params.down_delay); if (def->bond_params.fail_over_mac_policy) g_key_file_set_string(kf, "bond", "fail_over_mac", def->bond_params.fail_over_mac_policy); if (def->bond_params.gratuitous_arp) { g_key_file_set_integer(kf, "bond", "num_grat_arp", def->bond_params.gratuitous_arp); /* Work around issue in NM where unset unsolicited_na will overwrite num_grat_arp: * https://github.com/NetworkManager/NetworkManager/commit/42b0bef33c77a0921590b2697f077e8ea7805166 */ g_key_file_set_integer(kf, "bond", "num_unsol_na", def->bond_params.gratuitous_arp); } if (def->bond_params.packets_per_member) g_key_file_set_integer(kf, "bond", "packets_per_slave", def->bond_params.packets_per_member); /* wokeignore:rule=slave */ if (def->bond_params.primary_reselect_policy) g_key_file_set_string(kf, "bond", "primary_reselect", def->bond_params.primary_reselect_policy); if (def->bond_params.resend_igmp) g_key_file_set_integer(kf, "bond", "resend_igmp", def->bond_params.resend_igmp); if (def->bond_params.learn_interval) g_key_file_set_string(kf, "bond", "lp_interval", def->bond_params.learn_interval); if (def->bond_params.primary_member) g_key_file_set_string(kf, "bond", "primary", def->bond_params.primary_member); } STATIC void write_bridge_params_nm(const NetplanNetDefinition* def, GKeyFile *kf) { if (def->custom_bridging) { if (def->bridge_params.ageing_time) g_key_file_set_string(kf, "bridge", "ageing-time", def->bridge_params.ageing_time); if (def->bridge_params.priority) g_key_file_set_uint64(kf, "bridge", "priority", def->bridge_params.priority); if (def->bridge_params.forward_delay) g_key_file_set_string(kf, "bridge", "forward-delay", def->bridge_params.forward_delay); if (def->bridge_params.hello_time) g_key_file_set_string(kf, "bridge", "hello-time", def->bridge_params.hello_time); if (def->bridge_params.max_age) g_key_file_set_string(kf, "bridge", "max-age", def->bridge_params.max_age); g_key_file_set_boolean(kf, "bridge", "stp", def->bridge_params.stp); } } STATIC gboolean write_nm_wireguard_params(const NetplanNetDefinition* def, GKeyFile *kf, GError** error) { /* The key was already validated via validate_tunnel_grammar(), but we need * to differentiate between base64 key VS absolute path key-file. And a base64 * string could (theoretically) start with '/', so we use is_wireguard_key() * as well to check for more specific characteristics (if needed). */ if (def->tunnel.private_key) { if (def->tunnel.private_key[0] == '/' && !is_wireguard_key(def->tunnel.private_key)) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "%s: private key needs to be base64 encoded when using the NM backend\n", def->id); return FALSE; } else g_key_file_set_string(kf, "wireguard", "private-key", def->tunnel.private_key); } if (def->tunnel_private_key_flags != NETPLAN_KEY_FLAG_NONE) g_key_file_set_uint64(kf, "wireguard", "private-key-flags", def->tunnel_private_key_flags); if (def->tunnel.port) g_key_file_set_uint64(kf, "wireguard", "listen-port", def->tunnel.port); if (def->tunnel.fwmark) g_key_file_set_uint64(kf, "wireguard", "fwmark", def->tunnel.fwmark); if (def->wireguard_peers) { for (guint i = 0; i < def->wireguard_peers->len; i++) { NetplanWireguardPeer *peer = g_array_index (def->wireguard_peers, NetplanWireguardPeer*, i); g_assert(peer->public_key); g_autofree gchar* tmp_group = g_strdup_printf("wireguard-peer.%s", peer->public_key); if (peer->keepalive) g_key_file_set_integer(kf, tmp_group, "persistent-keepalive", peer->keepalive); if (peer->endpoint) g_key_file_set_string(kf, tmp_group, "endpoint", peer->endpoint); /* The key was already validated via validate_tunnel_grammar(), but we need * to differentiate between base64 key VS absolute path key-file. And a base64 * string could (theoretically) start with '/', so we use is_wireguard_key() * as well to check for more specific characteristics (if needed). */ if (peer->preshared_key) { if (peer->preshared_key[0] == '/' && !is_wireguard_key(peer->preshared_key)) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "%s: shared key needs to be base64 encoded when using the NM backend\n", def->id); return FALSE; } else { g_key_file_set_value(kf, tmp_group, "preshared-key", peer->preshared_key); g_key_file_set_uint64(kf, tmp_group, "preshared-key-flags", 0); } } if (peer->allowed_ips && peer->allowed_ips->len > 0) { const gchar* list[peer->allowed_ips->len]; for (guint j = 0; j < peer->allowed_ips->len; ++j) list[j] = g_array_index(peer->allowed_ips, char*, j); g_key_file_set_string_list(kf, tmp_group, "allowed-ips", list, peer->allowed_ips->len); } } } return TRUE; } STATIC void write_nm_tunnel_params(const NetplanNetDefinition* def, GKeyFile *kf) { g_key_file_set_integer(kf, "ip-tunnel", "mode", def->tunnel.mode); if (def->tunnel.local_ip) g_key_file_set_string(kf, "ip-tunnel", "local", def->tunnel.local_ip); g_key_file_set_string(kf, "ip-tunnel", "remote", def->tunnel.remote_ip); if (def->tunnel_ttl) g_key_file_set_uint64(kf, "ip-tunnel", "ttl", def->tunnel_ttl); if (def->tunnel.input_key) g_key_file_set_string(kf, "ip-tunnel", "input-key", def->tunnel.input_key); if (def->tunnel.output_key) g_key_file_set_string(kf, "ip-tunnel", "output-key", def->tunnel.output_key); } STATIC void write_dot1x_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) { switch (auth->eap_method) { case NETPLAN_AUTH_EAP_TLS: g_key_file_set_string(kf, "802-1x", "eap", "tls"); break; case NETPLAN_AUTH_EAP_PEAP: g_key_file_set_string(kf, "802-1x", "eap", "peap"); break; case NETPLAN_AUTH_EAP_TTLS: g_key_file_set_string(kf, "802-1x", "eap", "ttls"); break; case NETPLAN_AUTH_EAP_LEAP: g_key_file_set_string(kf, "802-1x", "eap", "leap"); break; case NETPLAN_AUTH_EAP_PWD: g_key_file_set_string(kf, "802-1x", "eap", "pwd"); break; default: break; // LCOV_EXCL_LINE } if (auth->identity) g_key_file_set_string(kf, "802-1x", "identity", auth->identity); if (auth->anonymous_identity) g_key_file_set_string(kf, "802-1x", "anonymous-identity", auth->anonymous_identity); /* auth->password might contain the PSK if it was defined inside the auth key in the YAML file. * We only write auth-password in [802-1x].password if it's not a PSK used by either PSK or SAE * or if an EAP method was defined. */ if (auth->password && (!_is_auth_key_management_psk(auth) || auth->eap_method != NETPLAN_AUTH_EAP_NONE)) g_key_file_set_string(kf, "802-1x", "password", auth->password); if (auth->ca_certificate) g_key_file_set_string(kf, "802-1x", "ca-cert", auth->ca_certificate); if (auth->client_certificate) g_key_file_set_string(kf, "802-1x", "client-cert", auth->client_certificate); if (auth->client_key) g_key_file_set_string(kf, "802-1x", "private-key", auth->client_key); if (auth->client_key_password) g_key_file_set_string(kf, "802-1x", "private-key-password", auth->client_key_password); if (auth->phase2_auth) g_key_file_set_string(kf, "802-1x", "phase2-auth", auth->phase2_auth); } STATIC void write_wifi_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) { switch (auth->key_management) { case NETPLAN_AUTH_KEY_MANAGEMENT_NONE: break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK: g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-psk"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP: case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSHA256: /* NM uses "wpa-eap" to enable both EAP and EAP-SHA256 */ g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-eap"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSUITE_B_192: g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-eap-suite-b-192"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_SAE: g_key_file_set_string(kf, "wifi-security", "key-mgmt", "sae"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_8021X: g_key_file_set_string(kf, "wifi-security", "key-mgmt", "ieee8021x"); break; default: break; // LCOV_EXCL_LINE } if (auth->key_management != NETPLAN_AUTH_KEY_MANAGEMENT_8021X) { switch (auth->pmf_mode) { case NETPLAN_AUTH_PMF_MODE_NONE: case NETPLAN_AUTH_PMF_MODE_DISABLED: break; case NETPLAN_AUTH_PMF_MODE_OPTIONAL: g_key_file_set_integer(kf, "wifi-security", "pmf", 2); break; case NETPLAN_AUTH_PMF_MODE_REQUIRED: g_key_file_set_integer(kf, "wifi-security", "pmf", 3); break; } } write_dot1x_auth_parameters(auth, kf); if (auth->psk) g_key_file_set_string(kf, "wifi-security", "psk", auth->psk); else if (auth->password && _is_auth_key_management_psk(auth)) g_key_file_set_string(kf, "wifi-security", "psk", auth->password); } STATIC void maybe_generate_uuid(const NetplanNetDefinition* def) { if (uuid_is_null(def->uuid)) uuid_generate((unsigned char*)def->uuid); } STATIC void write_nm_vxlan_parameters(const NetplanNetDefinition* def, GKeyFile* kf) { g_assert(def->vxlan); char uuidstr[37]; if (def->vxlan->ageing) g_key_file_set_uint64(kf, "vxlan", "ageing", def->vxlan->ageing); if (def->tunnel.port) g_key_file_set_uint64(kf, "vxlan", "destination-port", def->tunnel.port); if (def->vxlan->vni) g_key_file_set_uint64(kf, "vxlan", "id", def->vxlan->vni); if (def->vxlan->mac_learning != NETPLAN_TRISTATE_UNSET) g_key_file_set_boolean(kf, "vxlan", "learning", def->vxlan->mac_learning); if (def->vxlan->limit) g_key_file_set_uint64(kf, "vxlan", "limit", def->vxlan->limit); if (def->tunnel.local_ip) g_key_file_set_string(kf, "vxlan", "local", def->tunnel.local_ip); if (def->tunnel.remote_ip) g_key_file_set_string(kf, "vxlan", "remote", def->tunnel.remote_ip); if (def->vxlan->arp_proxy != NETPLAN_TRISTATE_UNSET) g_key_file_set_boolean(kf, "vxlan", "proxy", def->vxlan->arp_proxy); if (def->vxlan->notifications) { if (def->vxlan->notifications & NETPLAN_VXLAN_NOTIFICATION_L2_MISS) g_key_file_set_boolean(kf, "vxlan", "l2-miss", TRUE); if (def->vxlan->notifications & NETPLAN_VXLAN_NOTIFICATION_L3_MISS) g_key_file_set_boolean(kf, "vxlan", "l3-miss", TRUE); } if (def->vxlan->source_port_min && def->vxlan->source_port_max) { g_key_file_set_uint64(kf, "vxlan", "source-port-min", def->vxlan->source_port_min); g_key_file_set_uint64(kf, "vxlan", "source-port-max", def->vxlan->source_port_max); } if (def->vxlan->tos) g_key_file_set_uint64(kf, "vxlan", "tos", def->vxlan->tos); if (def->tunnel_ttl) g_key_file_set_uint64(kf, "vxlan", "ttl", def->tunnel_ttl); if (def->vxlan->short_circuit != NETPLAN_TRISTATE_UNSET) g_key_file_set_boolean(kf, "vxlan", "rsc", def->vxlan->short_circuit); if (def->vxlan->link) { if (def->vxlan->link->has_match) { /* we need to refer to the parent's UUID as we don't have an * interface name with match: */ maybe_generate_uuid(def->vxlan->link); uuid_unparse(def->vxlan->link->uuid, uuidstr); g_key_file_set_string(kf, "vxlan", "parent", uuidstr); } else { /* if we have an interface name, use that as parent */ g_key_file_set_string(kf, "vxlan", "parent", def->vxlan->link->id); } } if (def->vxlan->checksums || def->vxlan->extensions || def->vxlan->flow_label != G_MAXUINT || def->vxlan->do_not_fragment != NETPLAN_TRISTATE_UNSET) g_warning("%s: checksums/extensions/flow-lable/do-not-fragment are not supported by NetworkManager\n", def->id); } /** * Special handling for passthrough mode: read key-value pairs from * "backend_settings.passthrough" and inject them into the keyfile as-is. */ STATIC void write_fallback_key_value(GQuark key_id, gpointer value, gpointer user_data) { GKeyFile *kf = user_data; gchar* val = value; /* Group name may contain dots, but key name may not. * The "tc" group is a special case, where it is the other way around, e.g.: * tc->qdisc.root * tc->tfilter.ffff: */ const gchar* key = g_quark_to_string(key_id); gchar **group_key = g_strsplit(key, ".", -1); guint len = g_strv_length(group_key); g_autofree gchar* old_key = NULL; gboolean has_key = FALSE; g_autofree gchar* k = NULL; g_autofree gchar* group = NULL; if (!g_strcmp0(group_key[0], "tc") && len > 2) { k = g_strconcat(group_key[1], ".", group_key[2], NULL); group = g_strdup(group_key[0]); } else { k = group_key[len-1]; group_key[len-1] = NULL; //remove key from array group = g_strjoinv(".", group_key); //re-combine group parts } has_key = g_key_file_has_key(kf, group, k, NULL); old_key = g_key_file_get_string(kf, group, k, NULL); g_key_file_set_string(kf, group, k, val); /* delete the placeholder key, if this was just an empty group */ if (!g_strcmp0(k, NETPLAN_NM_EMPTY_GROUP)) g_key_file_remove_key(kf, group, k, NULL); /* handle differing defaults: * ipv6.ip6-privacy is "-1 (unknown)" by default in NM, it is "0 (off)" in netplan */ else if (g_strcmp0(key, "ipv6.ip6-privacy") == 0 && g_strcmp0(val, "-1") == 0) { g_debug("NetworkManager: default override: clearing %s.%s", group, k); g_key_file_remove_key(kf, group, k, NULL); } else if (!has_key) { g_debug("NetworkManager: passing through fallback key: %s.%s=%s", group, k, val); g_key_file_set_comment(kf, group, k, "Netplan: passthrough setting", NULL); } else if (!!g_strcmp0(val, old_key)) { g_debug("NetworkManager: fallback override: %s.%s=%s", group, k, val); g_key_file_set_comment(kf, group, k, "Netplan: passthrough override", NULL); } g_strfreev(group_key); } /** * Generate NetworkManager configuration in @rootdir/run/NetworkManager/ for a * particular NetplanNetDefinition and NetplanWifiAccessPoint, as NM requires a separate * connection file for each SSID. * @def: The NetplanNetDefinition for which to create a connection * @rootdir: If not %NULL, generate configuration in this root directory * (useful for testing). * @ap: The access point for which to create a connection. Must be %NULL for * non-wifi types. */ STATIC gboolean write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir, const NetplanWifiAccessPoint* ap, GError** error) { g_autoptr(GKeyFile) kf = NULL; g_autofree gchar* conf_path = NULL; g_autofree gchar* full_path = NULL; g_autofree gchar* nm_run_path = NULL; g_autofree gchar* nd_nm_id = NULL; const gchar* nm_type = NULL; gchar* tmp_key = NULL; mode_t orig_umask; char uuidstr[37]; const char *match_interface_name = NULL; if (def->type == NETPLAN_DEF_TYPE_WIFI) g_assert(ap); else g_assert(ap == NULL); if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) { g_debug("%s is defined as a hardware SR-IOV filtered VLAN, postponing creation", def->id); return TRUE; } kf = g_key_file_new(); if (ap && ap->backend_settings.name) g_key_file_set_string(kf, "connection", "id", ap->backend_settings.name); else if (def->backend_settings.name) g_key_file_set_string(kf, "connection", "id", def->backend_settings.name); else { /* Auto-generate a name for the connection profile, if not specified */ if (ap) nd_nm_id = g_strdup_printf("netplan-%s-%s", def->id, ap->ssid); else nd_nm_id = g_strdup_printf("netplan-%s", def->id); g_key_file_set_string(kf, "connection", "id", nd_nm_id); } nm_type = type_str(def); if (nm_type && def->type != NETPLAN_DEF_TYPE_NM) g_key_file_set_string(kf, "connection", "type", nm_type); if (ap && ap->backend_settings.uuid) g_key_file_set_string(kf, "connection", "uuid", ap->backend_settings.uuid); else if (def->backend_settings.uuid) g_key_file_set_string(kf, "connection", "uuid", def->backend_settings.uuid); /* VLAN/VXLAN devices refer to us as their parent; if our ID is not a name * but we have matches, parent= must be the connection UUID, so put it into * the connection */ if ((def->has_vlans || def->has_vxlans) && def->has_match) { maybe_generate_uuid(def); uuid_unparse(def->uuid, uuidstr); g_key_file_set_string(kf, "connection", "uuid", uuidstr); } if (def->activation_mode) { /* XXX: For now NetworkManager only supports the "manual" activation * mode */ if (!!g_strcmp0(def->activation_mode, "manual")) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: NetworkManager definitions do not support activation-mode %s\n", def->id, def->activation_mode); return FALSE; } /* "manual" */ g_key_file_set_boolean(kf, "connection", "autoconnect", FALSE); } if (def->type < NETPLAN_DEF_TYPE_VIRTUAL) { /* physical (existing) devices use matching; driver matching is not * supported, MAC matching is done below (different keyfile section), * so only match names here */ if (def->set_name) g_key_file_set_string(kf, "connection", "interface-name", def->set_name); else if (!def->has_match) g_key_file_set_string(kf, "connection", "interface-name", def->id); else if (def->match.original_name) { if (strpbrk(def->match.original_name, "*[]?")) match_interface_name = def->match.original_name; else g_key_file_set_string(kf, "connection", "interface-name", def->match.original_name); } /* else matches on something other than the name, do not restrict interface-name */ } else { /* virtual (created) devices set a name */ if (strnlen(def->id, IF_NAMESIZE) == IF_NAMESIZE) g_debug("interface-name %s is too long. Ignoring.", def->id); else g_key_file_set_string(kf, "connection", "interface-name", def->id); if (def->type == NETPLAN_DEF_TYPE_BRIDGE) write_bridge_params_nm(def, kf); if (def->type == NETPLAN_DEF_TYPE_VRF) { g_key_file_set_uint64(kf, "vrf", "table", def->vrf_table); if (!write_routes_nm(def, kf, AF_INET, error) || !write_routes_nm(def, kf, AF_INET6, error)) return FALSE; } if (def->type == NETPLAN_DEF_TYPE_VETH) { g_key_file_set_string(kf, "veth", "peer", def->veth_peer_link->id); } } if (def->type == NETPLAN_DEF_TYPE_MODEM) { const char* modem_type = modem_is_gsm(def) ? "gsm" : "cdma"; /* Use NetworkManager's auto configuration feature if no APN, username, or password is specified */ if (def->modem_params.auto_config || (!def->modem_params.apn && !def->modem_params.username && !def->modem_params.password)) { g_key_file_set_boolean(kf, modem_type, "auto-config", TRUE); } else { if (def->modem_params.apn) g_key_file_set_string(kf, modem_type, "apn", def->modem_params.apn); if (def->modem_params.password) g_key_file_set_string(kf, modem_type, "password", def->modem_params.password); if (def->modem_params.username) g_key_file_set_string(kf, modem_type, "username", def->modem_params.username); } if (def->modem_params.device_id) g_key_file_set_string(kf, modem_type, "device-id", def->modem_params.device_id); if (def->mtubytes) g_key_file_set_uint64(kf, modem_type, "mtu", def->mtubytes); if (def->modem_params.network_id) g_key_file_set_string(kf, modem_type, "network-id", def->modem_params.network_id); if (def->modem_params.number) g_key_file_set_string(kf, modem_type, "number", def->modem_params.number); if (def->modem_params.pin) g_key_file_set_string(kf, modem_type, "pin", def->modem_params.pin); if (def->modem_params.sim_id) g_key_file_set_string(kf, modem_type, "sim-id", def->modem_params.sim_id); if (def->modem_params.sim_operator_id) g_key_file_set_string(kf, modem_type, "sim-operator-id", def->modem_params.sim_operator_id); } if (def->bridge) { g_key_file_set_string(kf, "connection", "slave-type", "bridge"); /* wokeignore:rule=slave */ g_key_file_set_string(kf, "connection", "master", def->bridge); /* wokeignore:rule=master */ if (def->bridge_params.path_cost) g_key_file_set_uint64(kf, "bridge-port", "path-cost", def->bridge_params.path_cost); if (def->bridge_params.port_priority) g_key_file_set_uint64(kf, "bridge-port", "priority", def->bridge_params.port_priority); if (def->bridge_hairpin != NETPLAN_TRISTATE_UNSET) g_key_file_set_boolean(kf, "bridge-port", "hairpin-mode", def->bridge_hairpin); } if (def->bond) { g_key_file_set_string(kf, "connection", "slave-type", "bond"); /* wokeignore:rule=slave */ g_key_file_set_string(kf, "connection", "master", def->bond); /* wokeignore:rule=master */ } if (def->vrf_link) { g_key_file_set_string(kf, "connection", "slave-type", "vrf"); /* wokeignore:rule=slave */ g_key_file_set_string(kf, "connection", "master", def->vrf_link->id); /* wokeignore:rule=master */ } if (def->ipv6_mtubytes) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: NetworkManager definitions do not support ipv6-mtu\n", def->id); return FALSE; } if (def->type < NETPLAN_DEF_TYPE_VIRTUAL) { /* Avoid adding an [ethernet] section into the [gsm/cdma] description. */ if (g_strcmp0(nm_type, "gsm") != 0 || g_strcmp0(nm_type, "cdma") != 0) { if (g_strcmp0(nm_type, "ethernet") == 0) g_key_file_set_integer(kf, nm_type, "wake-on-lan", def->wake_on_lan ? 1 : 0); if (!def->set_name && def->match.mac) g_key_file_set_string(kf, nm_type, "mac-address", def->match.mac); if (def->set_mac) g_key_file_set_string(kf, nm_type, "cloned-mac-address", def->set_mac); if (def->mtubytes) g_key_file_set_uint64(kf, nm_type, "mtu", def->mtubytes); if (def->wowlan && def->wowlan > NETPLAN_WIFI_WOWLAN_DEFAULT) g_key_file_set_uint64(kf, nm_type, "wake-on-wlan", def->wowlan); if (def->ib_mode != NETPLAN_IB_MODE_KERNEL) g_key_file_set_string(kf, nm_type, "transport-mode", netplan_infiniband_mode_name(def->ib_mode)); } } else { if (def->set_mac) g_key_file_set_string(kf, "ethernet", "cloned-mac-address", def->set_mac); if (def->mtubytes) g_key_file_set_uint64(kf, "ethernet", "mtu", def->mtubytes); } if (def->type == NETPLAN_DEF_TYPE_VLAN) { g_assert(def->vlan_id < G_MAXUINT); g_assert(def->vlan_link != NULL); g_key_file_set_uint64(kf, "vlan", "id", def->vlan_id); if (def->vlan_link->has_match) { /* we need to refer to the parent's UUID as we don't have an * interface name with match: */ maybe_generate_uuid(def->vlan_link); uuid_unparse(def->vlan_link->uuid, uuidstr); g_key_file_set_string(kf, "vlan", "parent", uuidstr); } else { /* if we have an interface name, use that as parent */ g_key_file_set_string(kf, "vlan", "parent", def->vlan_link->id); } } if (def->type == NETPLAN_DEF_TYPE_BOND) write_nm_bond_parameters(def, kf); if (def->type == NETPLAN_DEF_TYPE_TUNNEL) { if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) { if (!write_nm_wireguard_params(def, kf, error)) return FALSE; } else if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_VXLAN) { write_nm_vxlan_parameters(def, kf); } else write_nm_tunnel_params(def, kf); } if (match_interface_name) { const gchar* list[1] = {match_interface_name}; g_key_file_set_string_list(kf, "match", "interface-name", list, 1); } if (ap && ap->mode == NETPLAN_WIFI_MODE_AP) g_key_file_set_string(kf, "ipv4", "method", "shared"); else if (def->dhcp4) g_key_file_set_string(kf, "ipv4", "method", "auto"); else if (def->ip4_addresses) /* This requires adding at least one address (done below) */ g_key_file_set_string(kf, "ipv4", "method", "manual"); else if (def->type == NETPLAN_DEF_TYPE_TUNNEL) /* sit tunnels will not start in link-local apparently */ g_key_file_set_string(kf, "ipv4", "method", "disabled"); else /* Without any address, this is the only available mode */ g_key_file_set_string(kf, "ipv4", "method", "link-local"); if (def->ip4_addresses) { for (unsigned i = 0; i < def->ip4_addresses->len; ++i) { tmp_key = g_strdup_printf("address%i", i+1); g_key_file_set_string(kf, "ipv4", tmp_key, g_array_index(def->ip4_addresses, char*, i)); g_free(tmp_key); } } if (def->gateway4) g_key_file_set_string(kf, "ipv4", "gateway", def->gateway4); if (def->ip4_nameservers) { const gchar* list[def->ip4_nameservers->len]; for (unsigned i = 0; i < def->ip4_nameservers->len; ++i) list[i] = g_array_index(def->ip4_nameservers, char*, i); g_key_file_set_string_list(kf, "ipv4", "dns", list, def->ip4_nameservers->len); } /* We can only write search domains and routes if we have an address */ if (def->ip4_addresses || def->dhcp4) { write_search_domains(def, "ipv4", kf); if (!write_routes_nm(def, kf, AF_INET, error)) return FALSE; } if (!def->dhcp4_overrides.use_routes) { g_key_file_set_boolean(kf, "ipv4", "ignore-auto-routes", TRUE); g_key_file_set_boolean(kf, "ipv4", "never-default", TRUE); } if (def->dhcp4 && def->dhcp4_overrides.metric != NETPLAN_METRIC_UNSPEC) g_key_file_set_uint64(kf, "ipv4", "route-metric", def->dhcp4_overrides.metric); if (def->dhcp6 || def->ip6_addresses || def->gateway6 || def->ip6_nameservers || def->ip6_addr_gen_mode) { g_key_file_set_string(kf, "ipv6", "method", def->dhcp6 ? "auto" : "manual"); if (def->ip6_addresses) { for (unsigned i = 0; i < def->ip6_addresses->len; ++i) { tmp_key = g_strdup_printf("address%i", i+1); g_key_file_set_string(kf, "ipv6", tmp_key, g_array_index(def->ip6_addresses, char*, i)); g_free(tmp_key); } } if (def->ip6_addr_gen_token) { /* Token implies EUI-64, i.e mode=0 */ g_key_file_set_integer(kf, "ipv6", "addr-gen-mode", 0); g_key_file_set_string(kf, "ipv6", "token", def->ip6_addr_gen_token); } else if (def->ip6_addr_gen_mode) g_key_file_set_string(kf, "ipv6", "addr-gen-mode", addr_gen_mode_str(def->ip6_addr_gen_mode)); if (def->ip6_privacy) g_key_file_set_integer(kf, "ipv6", "ip6-privacy", 2); else g_key_file_set_integer(kf, "ipv6", "ip6-privacy", 0); if (def->gateway6) g_key_file_set_string(kf, "ipv6", "gateway", def->gateway6); if (def->ip6_nameservers) { const gchar* list[def->ip6_nameservers->len]; for (unsigned i = 0; i < def->ip6_nameservers->len; ++i) list[i] = g_array_index(def->ip6_nameservers, char*, i); g_key_file_set_string_list(kf, "ipv6", "dns", list, def->ip6_nameservers->len); } /* nm-settings(5) specifies search-domain for both [ipv4] and [ipv6] -- * We need to specify it here for the IPv6-only case - see LP: #1786726 */ write_search_domains(def, "ipv6", kf); /* We can only write valid routes if there is a DHCPv6 or static IPv6 address */ if (!write_routes_nm(def, kf, AF_INET6, error)) return FALSE; if (!def->dhcp6_overrides.use_routes) { g_key_file_set_boolean(kf, "ipv6", "ignore-auto-routes", TRUE); g_key_file_set_boolean(kf, "ipv6", "never-default", TRUE); } if (def->dhcp6_overrides.metric != NETPLAN_METRIC_UNSPEC) g_key_file_set_uint64(kf, "ipv6", "route-metric", def->dhcp6_overrides.metric); } else g_key_file_set_string(kf, "ipv6", "method", "ignore"); if (def->backend_settings.passthrough) { g_debug("NetworkManager: using keyfile passthrough mode"); /* Write all key-value pairs from the hashtable into the keyfile, * potentially overriding existing values, if not fully supported. */ g_datalist_foreach((GData**)&def->backend_settings.passthrough, write_fallback_key_value, kf); } if (ap) { g_autofree char* escaped_ssid = g_uri_escape_string(ap->ssid, NULL, TRUE); /* TODO: make use of netplan_netdef_get_output_filename() */ conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, "-", escaped_ssid, ".nmconnection", NULL); g_key_file_set_string(kf, "wifi", "ssid", ap->ssid); if (ap->mode < NETPLAN_WIFI_MODE_OTHER) g_key_file_set_string(kf, "wifi", "mode", wifi_mode_str(ap->mode)); if (ap->bssid) g_key_file_set_string(kf, "wifi", "bssid", ap->bssid); if (ap->hidden) g_key_file_set_boolean(kf, "wifi", "hidden", TRUE); if (ap->band == NETPLAN_WIFI_BAND_5 || ap->band == NETPLAN_WIFI_BAND_24) { g_key_file_set_string(kf, "wifi", "band", wifi_band_str(ap->band)); /* Channel is only unambiguous, if band is set. */ if (ap->channel) { /* Validate WiFi channel */ if (ap->band == NETPLAN_WIFI_BAND_5) wifi_get_freq5(ap->channel); else wifi_get_freq24(ap->channel); g_key_file_set_uint64(kf, "wifi", "channel", ap->channel); } } if (ap->has_auth) { write_wifi_auth_parameters(&ap->auth, kf); } if (ap->backend_settings.passthrough) { g_debug("NetworkManager: using AP keyfile passthrough mode"); /* Write all key-value pairs from the hashtable into the keyfile, * potentially overriding existing values, if not fully supported. * AP passthrough values have higher priority than ND passthrough, * because they are more specific and bound to the current SSID's * NM connection profile. */ g_datalist_foreach((GData**)&ap->backend_settings.passthrough, write_fallback_key_value, kf); } } else { /* TODO: make use of netplan_netdef_get_output_filename() */ conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, ".nmconnection", NULL); if (def->has_auth) { write_dot1x_auth_parameters(&def->auth, kf); } } /* Create /run/NetworkManager/ with 755 permissions if the folder is missing. * Letting the next invokation of _netplan_safe_mkdir_p_dir do it would * result in more restrictive access because of the call to umask. */ nm_run_path = g_strjoin(G_DIR_SEPARATOR_S, rootdir ?: "", "run/NetworkManager/", NULL); if (!g_file_test(nm_run_path, G_FILE_TEST_EXISTS)) _netplan_safe_mkdir_p_dir(nm_run_path); full_path = g_strjoin(G_DIR_SEPARATOR_S, rootdir ?: "", conf_path, NULL); /* NM connection files might contain secrets, and NM insists on tight permissions */ orig_umask = umask(077); _netplan_safe_mkdir_p_dir(full_path); if (!g_key_file_save_to_file(kf, full_path, error)) return FALSE; // LCOV_EXCL_LINE umask(orig_umask); return TRUE; } /** * Generate NetworkManager configuration in @rootdir/run/NetworkManager/ for a * particular NetplanNetDefinition. * @rootdir: If not %NULL, generate configuration in this root directory * (useful for testing). */ gboolean _netplan_netdef_write_nm( __unused const NetplanState* np_state, const NetplanNetDefinition* netdef, const char* rootdir, gboolean* has_been_written, GError** error) { gboolean no_error = TRUE; /* Placeholder interfaces are not supposed to be rendered */ if (netdef->type == NETPLAN_DEF_TYPE_NM_PLACEHOLDER_) return TRUE; SET_OPT_OUT_PTR(has_been_written, FALSE); if (netdef->backend != NETPLAN_BACKEND_NM) { g_debug("NetworkManager: definition %s is not for us (backend %i)", netdef->id, netdef->backend); return TRUE; } if (netdef->match.driver && !netdef->set_name) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: NetworkManager definitions do not support matching by driver\n", netdef->id); return FALSE; } if (netdef->address_options) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: NetworkManager does not support address options\n", netdef->id); return FALSE; } if (netdef->type == NETPLAN_DEF_TYPE_VETH) { /* * Final validation of veths that can't be fully done during parsing due to the * order the interfaces are parsed. */ if (!validate_veth_pair(np_state, netdef, error)) return FALSE; } if (netdef->type == NETPLAN_DEF_TYPE_WIFI) { GHashTableIter iter; gpointer key; const NetplanWifiAccessPoint* ap; g_assert(netdef->access_points); g_hash_table_iter_init(&iter, netdef->access_points); while (g_hash_table_iter_next(&iter, &key, (gpointer) &ap) && no_error) no_error = write_nm_conf_access_point(netdef, rootdir, ap, error); } else { g_assert(netdef->access_points == NULL); no_error = write_nm_conf_access_point(netdef, rootdir, NULL, error); } SET_OPT_OUT_PTR(has_been_written, TRUE); return no_error; } gboolean netplan_state_finish_nm_write( const NetplanState* np_state, const char* rootdir, __unused GError** error) { GString* udev_rules = g_string_new(NULL); GString* nm_conf = g_string_new(NULL); if (netplan_state_get_netdefs_size(np_state) == 0) { g_string_free(udev_rules, TRUE); g_string_free(nm_conf, TRUE); return TRUE; } /* Set all devices not managed by us to unmanaged, so that NM does not * auto-connect and interferes. * Also, mark all devices managed by us explicitly, so it won't get in * conflict with the system's udev rules that might ignore some devices * in containers via usr/lib/udev/rules.d/85-nm-unmanaged-devices.rules */ GList* iter = np_state->netdefs_ordered; while (iter) { const NetplanNetDefinition* nd = iter->data; const gchar* nm_type; GString *tmp = NULL; guint unmanaged = nd->backend == NETPLAN_BACKEND_NM ? 0 : 1; /* Special case: manage or ignore any device of given type on empty "match: {}" stanza */ if (nd->has_match && !nd->match.driver && !nd->match.mac && !nd->match.original_name) { nm_type = type_str(nd); g_assert(nm_type); g_string_append_printf(nm_conf, "[device-netplan.%s.%s]\nmatch-device=type:%s\n" "managed=%d\n\n", netplan_def_type_name(nd->type), nd->id, nm_type, !unmanaged); } /* Normal case: manage or ignore devices by specific udev rules */ else { const gchar *prefix = "SUBSYSTEM==\"net\", ACTION==\"add|change|move\","; const gchar *suffix = nd->backend == NETPLAN_BACKEND_NM ? " ENV{NM_UNMANAGED}=\"0\"\n" : " ENV{NM_UNMANAGED}=\"1\"\n"; g_string_append_printf(udev_rules, "# netplan: network.%s.%s (on NetworkManager %s)\n", netplan_def_type_name(nd->type), nd->id, unmanaged ? "deny-list" : "allow-list"); /* Match by explicit interface name, if possible */ if (nd->set_name) { // simple case: explicit new interface name g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, nd->set_name, suffix); } else if (!nd->has_match) { // simple case: explicit netplan ID is interface name g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, nd->id, suffix); } /* Also, match by explicit (new) MAC, if available */ if (nd->set_mac && _is_valid_macaddress(nd->set_mac)) { tmp = g_string_new(nd->set_mac); g_string_append_printf(udev_rules, "%s ATTR{address}==\"%s\",%s", prefix, g_string_ascii_down(tmp)->str, suffix); g_string_free(tmp, TRUE); } /* Finally, add a full match, using all rules & globs available * from the "match" stanza (e.g. original_name/mac/drivers) * This will match the "old" interface (i.e. original MAC and/or * interface name) if it got changed */ if (nd->has_match && (nd->match.original_name || nd->match.mac || nd->match.driver)) { // match on original name glob // TODO: maybe support matching on multiple name globs in the future (like drivers) g_string_append(udev_rules, prefix); if (nd->match.original_name) g_string_append_printf(udev_rules, " ENV{ID_NET_NAME}==\"%s\",", nd->match.original_name); // match on (explicit) MAC address. Yes this would be unique on its own, but we // keep it within the "full match" to make the logic more comprehensible. if (nd->match.mac) { tmp = g_string_new(nd->match.mac); g_string_append_printf(udev_rules, " ATTR{address}==\"%s\",", g_string_ascii_down(tmp)->str); g_string_free(tmp, TRUE); } // match on (multiple) driver globs if (nd->match.driver) { gchar *drivers = NULL; if (strchr(nd->match.driver, '\t')) { gchar **split = g_strsplit(nd->match.driver, "\t", -1); drivers = g_strjoinv("|", split); g_strfreev(split); } else drivers = g_strdup(nd->match.driver); g_string_append_printf(udev_rules, " ENV{ID_NET_DRIVER}==\"%s\",", drivers); g_free(drivers); } g_string_append(udev_rules, suffix); } } iter = iter->next; } /* write generated NetworkManager drop-in config */ if (nm_conf->len > 0) _netplan_g_string_free_to_file(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL); else g_string_free(nm_conf, TRUE); /* write generated udev rules */ if (udev_rules->len > 0) _netplan_g_string_free_to_file(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL); else g_string_free(udev_rules, TRUE); return TRUE; } /** * Clean up all generated configurations in @rootdir from previous runs. */ gboolean _netplan_nm_cleanup(const char* rootdir) { g_autofree char* confpath = g_strjoin(NULL, rootdir ?: "", "/run/NetworkManager/conf.d/netplan.conf", NULL); g_autofree char* global_manage_path = g_strjoin(NULL, rootdir ?: "", "/run/NetworkManager/conf.d/10-globally-managed-devices.conf", NULL); unlink(confpath); unlink(global_manage_path); _netplan_unlink_glob(rootdir, "/run/NetworkManager/system-connections/netplan-*"); return TRUE; } netplan-1.0/src/nm.h000066400000000000000000000017601457004145200143730ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * Author: Martin Pitt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "netplan.h" NETPLAN_INTERNAL gboolean _netplan_netdef_write_nm( const NetplanState* np_state, const NetplanNetDefinition* netdef, const char* rootdir, gboolean* has_been_written, GError** error); NETPLAN_INTERNAL gboolean _netplan_nm_cleanup(const char* rootdir); netplan-1.0/src/openvswitch.c000066400000000000000000000535471457004145200163370ustar00rootroot00000000000000/* * Copyright (C) 2020 Canonical, Ltd. * Author: Łukasz 'sil2100' Zemczak * Lukas 'slyon' Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include "openvswitch.h" #include "networkd.h" #include "parse.h" #include "util.h" #include "util-internal.h" STATIC gboolean write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, gboolean physical, gboolean cleanup, const char* dependency, GError** error) { g_autofree gchar* id_escaped = NULL; g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-", id, ".service", NULL); g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-ovs-", id, ".service", NULL); GString* s = g_string_new("[Unit]\n"); g_string_append_printf(s, "Description=OpenVSwitch configuration for %s\n", id); g_string_append(s, "DefaultDependencies=no\n"); /* run any ovs-netplan unit only after openvswitch-switch.service is ready */ g_string_append_printf(s, "Wants=ovsdb-server.service\n"); g_string_append_printf(s, "After=ovsdb-server.service\n"); if (physical) { id_escaped = systemd_escape((char*) id); g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", id_escaped); g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", id_escaped); } if (!cleanup) { g_string_append_printf(s, "After=netplan-ovs-cleanup.service\n"); } else { /* The netplan-ovs-cleanup unit shall not run on systems where Open vSwitch is not installed. */ g_string_append(s, "ConditionFileIsExecutable=" OPENVSWITCH_OVS_VSCTL "\n"); } g_string_append(s, "Before=network.target\nWants=network.target\n"); if (dependency) { g_string_append_printf(s, "Requires=netplan-ovs-%s.service\n", dependency); g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency); } g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n"); /* During tests the rate in which the netplan-ovs-cleanup service is started/stopped * might exceed StartLimitBurst. */ if (cleanup) g_string_append(s, "StartLimitBurst=0\n"); g_string_append(s, cmds->str); _netplan_g_string_free_to_file(s, rootdir, path, NULL); _netplan_safe_mkdir_p_dir(link); if (symlink(path, link) < 0 && errno != EEXIST) { // LCOV_EXCL_START g_set_error(error, NETPLAN_FILE_ERROR, errno, "failed to create enablement symlink: %m\n"); return FALSE; // LCOV_EXCL_STOP } return TRUE; } #define append_systemd_cmd(s, command, ...) \ { \ g_string_append(s, "ExecStart="); \ g_string_append_printf(s, command, __VA_ARGS__); \ g_string_append(s, "\n"); \ } STATIC char* netplan_type_to_table_name(const NetplanDefType type) { switch (type) { case NETPLAN_DEF_TYPE_BRIDGE: return "Bridge"; case NETPLAN_DEF_TYPE_BOND: case NETPLAN_DEF_TYPE_PORT: return "Port"; default: /* For regular interfaces and others */ return "Interface"; } } STATIC gboolean netplan_type_is_physical(const NetplanDefType type) { switch (type) { case NETPLAN_DEF_TYPE_ETHERNET: // case NETPLAN_DEF_TYPE_WIFI: // case NETPLAN_DEF_TYPE_MODEM: return TRUE; default: return FALSE; } } STATIC void write_ovs_tag_setting(const gchar* id, const char* type, const char* col, const char* key, const char* value, GString* cmds) { g_assert(col); g_assert(value); g_autofree char *clean_value = g_strdup(value); /* Replace " " -> "," if value contains spaces */ if (strchr(value, ' ')) { char **split = g_strsplit(value, " ", -1); g_free(clean_value); clean_value = g_strjoinv(",", split); g_strfreev(split); } GString* s = g_string_new("external-ids:netplan/"); g_string_append_printf(s, "%s", col); if (key) g_string_append_printf(s, "/%s", key); g_string_append_printf(s, "=%s", clean_value); append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set %s %s %s", type, id, s->str); g_string_free(s, TRUE); } STATIC void write_ovs_additional_data(GHashTable *data, const char* type, const gchar* id, GString* cmds, const char* setting) { GHashTableIter iter; gchar* key; gchar* value; g_hash_table_iter_init(&iter, data); while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &value)) { /* XXX: we need to check what happens when an invalid key=value pair gets supplied here. We might want to handle this somehow. */ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set %s %s %s:%s=%s", type, id, setting, key, value); write_ovs_tag_setting(id, type, setting, key, value, cmds); } } STATIC void setup_patch_port(GString* s, const NetplanNetDefinition* def) { /* Execute the setup commands to create an OVS patch port atomically within * the same command where this virtual interface is created. Either as a * Port+Interface of an OVS bridge or as a Interface of an OVS bond. This * avoids delays in the PatchPort creation and thus potential races. */ g_assert(def->type == NETPLAN_DEF_TYPE_PORT); g_string_append_printf(s, " -- set Interface %s type=patch options:peer=%s", def->id, def->peer); } STATIC char* write_ovs_bond_interfaces(const NetplanState* np_state, const NetplanNetDefinition* def, GString* cmds, GError** error) { NetplanNetDefinition* tmp_nd; GHashTableIter iter; gchar* key; guint i = 0; g_autoptr(GString) s = NULL; g_autoptr(GString) patch_ports = g_string_new(""); if (!def->bridge) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "Bond %s needs to be a member of an OpenVSwitch bridge\n", def->id); return NULL; } s = g_string_new(OPENVSWITCH_OVS_VSCTL " --may-exist add-bond"); g_string_append_printf(s, " %s %s", def->bridge, def->id); g_hash_table_iter_init(&iter, np_state->netdefs); while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &tmp_nd)) { if (!g_strcmp0(def->id, tmp_nd->bond)) { /* Append and count bond interfaces */ g_string_append_printf(s, " %s", tmp_nd->id); i++; if (tmp_nd->type == NETPLAN_DEF_TYPE_PORT) setup_patch_port(patch_ports, tmp_nd); } } if (i < 2) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "Bond %s needs to have at least 2 member interfaces\n", def->id); return NULL; } g_string_append(s, patch_ports->str); append_systemd_cmd(cmds, s->str, def->bridge, def->id); return def->bridge; } STATIC void write_ovs_tag_netplan(const gchar* id, const char* type, GString* cmds) { /* Mark this bridge/port/interface as created by netplan */ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set %s %s external-ids:netplan=true", type, id); } STATIC gboolean write_ovs_bond_mode(const NetplanNetDefinition* def, GString* cmds, GError** error) { char* value = NULL; /* OVS supports only "active-backup", "balance-tcp" and "balance-slb": * http://www.openvswitch.org/support/dist-docs/ovs-vswitchd.conf.db.5.txt */ if (!strcmp(def->bond_params.mode, "active-backup") || !strcmp(def->bond_params.mode, "balance-tcp") || !strcmp(def->bond_params.mode, "balance-slb")) { value = def->bond_params.mode; append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Port %s bond_mode=%s", def->id, value); write_ovs_tag_setting(def->id, "Port", "bond_mode", NULL, value, cmds); } else { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "%s: bond mode '%s' not supported by Open vSwitch\n", def->id, def->bond_params.mode); return FALSE; } return TRUE; } STATIC void write_ovs_bridge_interfaces(const NetplanState* np_state, const NetplanNetDefinition* def, GString* cmds) { NetplanNetDefinition* tmp_nd; GHashTableIter iter; gchar* key; append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " --may-exist add-br %s", def->id); g_hash_table_iter_init(&iter, np_state->netdefs); while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &tmp_nd)) { /* OVS bonds will connect to their OVS bridge and create the interface/port themselves */ if ((tmp_nd->type != NETPLAN_DEF_TYPE_BOND || tmp_nd->backend != NETPLAN_BACKEND_OVS) && !g_strcmp0(def->id, tmp_nd->bridge)) { GString * patch_ports = g_string_new(""); if (tmp_nd->type == NETPLAN_DEF_TYPE_PORT) setup_patch_port(patch_ports, tmp_nd); append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " --may-exist add-port %s %s%s", def->id, tmp_nd->id, patch_ports->str); g_string_free(patch_ports, TRUE); } } } STATIC void write_ovs_protocols(const NetplanOVSSettings* ovs_settings, const gchar* bridge, GString* cmds) { g_assert(bridge); GString* s = g_string_new(g_array_index(ovs_settings->protocols, char*, 0)); for (unsigned i = 1; i < ovs_settings->protocols->len; ++i) g_string_append_printf(s, ",%s", g_array_index(ovs_settings->protocols, char*, i)); append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Bridge %s protocols=%s", bridge, s->str); write_ovs_tag_setting(bridge, "Bridge", "protocols", NULL, s->str, cmds); g_string_free(s, TRUE); } STATIC gboolean check_ovs_ssl(const NetplanOVSSettings* settings, gchar* target, gboolean* needs_ssl, GError** error) { /* Check if target needs ssl */ if (g_str_has_prefix(target, "ssl:") || g_str_has_prefix(target, "pssl:")) { /* Check if SSL is configured in settings->ssl */ if (!settings->ssl.ca_certificate || !settings->ssl.client_certificate || !settings->ssl.client_key) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "ERROR: Open vSwitch bridge controller target '%s' needs SSL configuration, but global 'openvswitch.ssl' settings are not set\n", target); return FALSE; } *needs_ssl = TRUE; return TRUE; } *needs_ssl = FALSE; return TRUE; } STATIC gboolean write_ovs_bridge_controller_targets(const NetplanOVSSettings* settings, const NetplanOVSController* controller, const gchar* bridge, GString* cmds, GError** error) { gchar* target = g_array_index(controller->addresses, char*, 0); GString* s = g_string_sized_new(0); gboolean ret = TRUE; gboolean needs_ssl = FALSE; for (unsigned i = 0; i < controller->addresses->len; ++i) { target = g_array_index(controller->addresses, char*, i); if (!needs_ssl) if (!check_ovs_ssl(settings, target, &needs_ssl, error)) { ret = FALSE; goto cleanup; } g_string_append_printf(s, "%s ", target); } g_string_erase(s, s->len-1, 1); append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set-controller %s %s", bridge, s->str); write_ovs_tag_setting(bridge, "Bridge", "global", "set-controller", s->str, cmds); cleanup: g_string_free(s, TRUE); return ret; } /** * Generate the OpenVSwitch systemd units for configuration of the selected netdef * @rootdir: If not %NULL, generate configuration in this root directory * (useful for testing). */ gboolean _netplan_netdef_write_ovs(const NetplanState* np_state, const NetplanNetDefinition* def, const char* rootdir, gboolean* has_been_written, GError** error) { g_autoptr(GString) cmds = g_string_new(NULL); gchar* dependency = NULL; const char* type = netplan_type_to_table_name(def->type); g_autofree char* base_config_path = NULL; char* value = NULL; const NetplanOVSSettings* settings = &np_state->ovs_settings; SET_OPT_OUT_PTR(has_been_written, FALSE); /* TODO: maybe dynamically query the ovs-vsctl tool path? */ /* For OVS specific settings, we expect the backend to be set to OVS. * The OVS backend is implicitly set, if an interface contains an empty "openvswitch: {}" * key, or an "openvswitch:" key, containing more than "external-ids" and/or "other-config". */ if (def->backend == NETPLAN_BACKEND_OVS) { switch (def->type) { case NETPLAN_DEF_TYPE_BOND: dependency = write_ovs_bond_interfaces(np_state, def, cmds, error); if (!dependency) return FALSE; write_ovs_tag_netplan(def->id, type, cmds); /* Set LACP mode, default to "off" */ value = def->ovs_settings.lacp? def->ovs_settings.lacp : "off"; append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Port %s lacp=%s", def->id, value); write_ovs_tag_setting(def->id, type, "lacp", NULL, value, cmds); if (def->bond_params.mode && !write_ovs_bond_mode(def, cmds, error)) return FALSE; break; case NETPLAN_DEF_TYPE_BRIDGE: write_ovs_bridge_interfaces(np_state, def, cmds); write_ovs_tag_netplan(def->id, type, cmds); /* Set fail-mode, default to "standalone" */ value = def->ovs_settings.fail_mode? def->ovs_settings.fail_mode : "standalone"; append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set-fail-mode %s %s", def->id, value); write_ovs_tag_setting(def->id, type, "global", "set-fail-mode", value, cmds); /* Enable/disable mcast-snooping */ value = def->ovs_settings.mcast_snooping? "true" : "false"; append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Bridge %s mcast_snooping_enable=%s", def->id, value); write_ovs_tag_setting(def->id, type, "mcast_snooping_enable", NULL, value, cmds); /* Enable/disable rstp */ value = def->ovs_settings.rstp? "true" : "false"; append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Bridge %s rstp_enable=%s", def->id, value); write_ovs_tag_setting(def->id, type, "rstp_enable", NULL, value, cmds); /* Set protocols */ if (def->ovs_settings.protocols && def->ovs_settings.protocols->len > 0) write_ovs_protocols(&(def->ovs_settings), def->id, cmds); else if (settings->protocols && settings->protocols->len > 0) write_ovs_protocols(settings, def->id, cmds); /* Set controller target addresses */ if (def->ovs_settings.controller.addresses && def->ovs_settings.controller.addresses->len > 0) { if (!write_ovs_bridge_controller_targets(settings, &(def->ovs_settings.controller), def->id, cmds, error)) return FALSE; /* Set controller connection mode, only applicable if at least one controller target address was set */ if (def->ovs_settings.controller.connection_mode) { value = def->ovs_settings.controller.connection_mode; append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Controller %s connection-mode=%s", def->id, value); write_ovs_tag_setting(def->id, "Controller", "connection-mode", NULL, value, cmds); } } break; case NETPLAN_DEF_TYPE_PORT: g_assert(def->peer); dependency = def->bridge?: def->bond; if (!dependency) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "%s: OpenVSwitch patch port needs to be assigned to a bridge/bond\n", def->id); return FALSE; } /* There is no OVS Port which we could tag netplan=true if this * patch port is assigned as an OVS bond interface. Tag the * Interface instead, to clean it up from a bond. */ if (def->bond) write_ovs_tag_netplan(def->id, "Interface", cmds); else write_ovs_tag_netplan(def->id, type, cmds); break; case NETPLAN_DEF_TYPE_VLAN: g_assert(def->vlan_link); dependency = def->vlan_link->id; /* Create a fake VLAN bridge */ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " --may-exist add-br %s %s %i", def->id, def->vlan_link->id, def->vlan_id) write_ovs_tag_netplan(def->id, type, cmds); break; default: g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "%s: This device type is not supported with the OpenVSwitch backend\n", def->id); return FALSE; } /* Try writing out a base config */ /* TODO: make use of netplan_netdef_get_output_filename() */ base_config_path = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL); if (!_netplan_netdef_write_network_file(np_state, def, rootdir, base_config_path, has_been_written, error)) return FALSE; } else { /* Other interfaces must be part of an OVS bridge or bond to carry additional data */ if ( (def->ovs_settings.external_ids && g_hash_table_size(def->ovs_settings.external_ids) > 0) || (def->ovs_settings.other_config && g_hash_table_size(def->ovs_settings.other_config) > 0)) { dependency = def->bridge?: def->bond; if (!dependency) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "%s: Interface needs to be assigned to an OVS bridge/bond to carry external-ids/other-config\n", def->id); return FALSE; } } else { g_debug("Open vSwitch: definition %s is not for us (backend %i)", def->id, def->backend); SET_OPT_OUT_PTR(has_been_written, FALSE); return TRUE; } } /* Set "external-ids" and "other-config" after NETPLAN_BACKEND_OVS interfaces, as bonds, * bridges, etc. might just be created before.*/ /* Common OVS settings can be specified even for non-OVS interfaces */ if (def->ovs_settings.external_ids && g_hash_table_size(def->ovs_settings.external_ids) > 0) { write_ovs_additional_data(def->ovs_settings.external_ids, type, def->id, cmds, "external-ids"); } if (def->ovs_settings.other_config && g_hash_table_size(def->ovs_settings.other_config) > 0) { write_ovs_additional_data(def->ovs_settings.other_config, type, def->id, cmds, "other-config"); } /* If we need to configure anything for this netdef, write the required systemd unit */ gboolean ret = TRUE; if (cmds->len > 0) ret = write_ovs_systemd_unit(def->id, cmds, rootdir, netplan_type_is_physical(def->type), FALSE, dependency, error); SET_OPT_OUT_PTR(has_been_written, TRUE); return ret; } /** * Finalize the OpenVSwitch configuration (global config) */ gboolean netplan_state_finish_ovs_write(const NetplanState* np_state, const char* rootdir, GError** error) { const NetplanOVSSettings* settings = &np_state->ovs_settings; GString* cmds = g_string_new(NULL); /* Global external-ids and other-config settings */ if (settings->external_ids && g_hash_table_size(settings->external_ids) > 0) write_ovs_additional_data(settings->external_ids, "open_vswitch", ".", cmds, "external-ids"); if (settings->other_config && g_hash_table_size(settings->other_config) > 0) write_ovs_additional_data(settings->other_config, "open_vswitch", ".", cmds, "other-config"); if (settings->ssl.client_key && settings->ssl.client_certificate && settings->ssl.ca_certificate) { GString* value = g_string_new(NULL); g_string_printf(value, "%s %s %s", settings->ssl.client_key, settings->ssl.client_certificate, settings->ssl.ca_certificate); append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set-ssl %s", value->str); write_ovs_tag_setting(".", "open_vswitch", "global", "set-ssl", value->str, cmds); g_string_free(value, TRUE); } gboolean ret = TRUE; if (cmds->len > 0) ret = write_ovs_systemd_unit("global", cmds, rootdir, FALSE, FALSE, NULL, error); g_string_free(cmds, TRUE); if (!ret) return FALSE; // LCOV_EXCL_LINE /* Clear all netplan=true tagged ports/bonds and bridges, via 'netplan apply --only-ovs-cleanup' */ cmds = g_string_new(NULL); append_systemd_cmd(cmds, SBINDIR "/netplan apply %s", "--only-ovs-cleanup"); ret = write_ovs_systemd_unit("cleanup", cmds, rootdir, FALSE, TRUE, NULL, error); g_string_free(cmds, TRUE); return ret; } /** * Clean up all generated configurations in @rootdir from previous runs. */ gboolean _netplan_ovs_cleanup(const char* rootdir) { _netplan_unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-*.service"); _netplan_unlink_glob(rootdir, "/run/systemd/system/netplan-ovs-*.service"); return TRUE; } netplan-1.0/src/openvswitch.h000066400000000000000000000020031457004145200163210ustar00rootroot00000000000000/* * Copyright (C) 2020 Canonical, Ltd. * Author: Łukasz 'sil2100' Zemczak * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "netplan.h" NETPLAN_INTERNAL gboolean _netplan_netdef_write_ovs( const NetplanState* np_state, const NetplanNetDefinition* netdef, const char* rootdir, gboolean* has_been_written, GError** error); NETPLAN_INTERNAL gboolean _netplan_ovs_cleanup(const char* rootdir); netplan-1.0/src/parse-nm.c000066400000000000000000001267661457004145200155140ustar00rootroot00000000000000/* * Copyright (C) 2021 Canonical, Ltd. * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include "netplan.h" #include "parse-nm.h" #include "parse.h" #include "util.h" #include "types-internal.h" #include "util-internal.h" #include "validation.h" /** * NetworkManager writes the alias for '802-3-ethernet' (ethernet), * '802-11-wireless' (wifi) and '802-11-wireless-security' (wifi-security) * by default, so we only need to check for those. See: * https://bugzilla.gnome.org/show_bug.cgi?id=696940 * https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/c36200a225aefb2a3919618e75682646899b82c0 */ STATIC NetplanDefType type_from_str(const char* type_str) { if (!g_strcmp0(type_str, "ethernet") || !g_strcmp0(type_str, "802-3-ethernet")) return NETPLAN_DEF_TYPE_ETHERNET; else if (!g_strcmp0(type_str, "wifi") || !g_strcmp0(type_str, "802-11-wireless")) return NETPLAN_DEF_TYPE_WIFI; else if (!g_strcmp0(type_str, "gsm") || !g_strcmp0(type_str, "cdma")) return NETPLAN_DEF_TYPE_MODEM; else if (!g_strcmp0(type_str, "bridge")) return NETPLAN_DEF_TYPE_BRIDGE; else if (!g_strcmp0(type_str, "bond")) return NETPLAN_DEF_TYPE_BOND; else if (!g_strcmp0(type_str, "dummy")) /* wokeignore:rule=dummy */ return NETPLAN_DEF_TYPE_DUMMY; /* wokeignore:rule=dummy */ else if (!g_strcmp0(type_str, "veth")) return NETPLAN_DEF_TYPE_VETH; else if (!g_strcmp0(type_str, "vlan")) return NETPLAN_DEF_TYPE_VLAN; else if (!g_strcmp0(type_str, "vrf")) return NETPLAN_DEF_TYPE_VRF; else if ( !g_strcmp0(type_str, "wireguard") || !g_strcmp0(type_str, "vxlan") || !g_strcmp0(type_str, "ip-tunnel")) return NETPLAN_DEF_TYPE_TUNNEL; /* Unsupported type, needs to be specified via passthrough */ return NETPLAN_DEF_TYPE_NM; } STATIC NetplanWifiMode ap_type_from_str(const char* type_str) { if (!g_strcmp0(type_str, "infrastructure")) return NETPLAN_WIFI_MODE_INFRASTRUCTURE; else if (!g_strcmp0(type_str, "ap")) return NETPLAN_WIFI_MODE_AP; else if (!g_strcmp0(type_str, "adhoc")) return NETPLAN_WIFI_MODE_ADHOC; /* Unsupported mode, like "mesh" */ return NETPLAN_WIFI_MODE_OTHER; } STATIC NetplanTunnelMode tunnel_mode_from_str(const char* type_str) { if (!g_strcmp0(type_str, "wireguard")) return NETPLAN_TUNNEL_MODE_WIREGUARD; else if (!g_strcmp0(type_str, "vxlan")) return NETPLAN_TUNNEL_MODE_VXLAN; return NETPLAN_TUNNEL_MODE_UNKNOWN; } STATIC void _kf_clear_key(GKeyFile* kf, const gchar* group, const gchar* key) { gsize len = 1; g_key_file_remove_key(kf, group, key, NULL); g_strfreev(g_key_file_get_keys(kf, group, &len, NULL)); /* clear group if this was the last key */ if (len == 0) g_key_file_remove_group(kf, group, NULL); } STATIC gboolean kf_matches(GKeyFile* kf, const gchar* group, const gchar* key, const gchar* match) { g_autofree gchar *kf_value = g_key_file_get_string(kf, group, key, NULL); return g_strcmp0(kf_value, match) == 0; } STATIC void set_true_on_match(GKeyFile* kf, const gchar* group, const gchar* key, const gchar* match, const void* dataptr) { g_assert(dataptr); if (kf_matches(kf, group, key, match)) { *((gboolean*) dataptr) = TRUE; _kf_clear_key(kf, group, key); } } STATIC void keyfile_handle_generic_bool(GKeyFile* kf, const gchar* group, const gchar* key, gboolean* dataptr) { g_assert(dataptr); *dataptr = g_key_file_get_boolean(kf, group, key, NULL); _kf_clear_key(kf, group, key); } STATIC void keyfile_handle_generic_str(GKeyFile* kf, const gchar* group, const gchar* key, char** dataptr) { g_assert(dataptr); g_assert(!*dataptr); *dataptr = g_key_file_get_string(kf, group, key, NULL); if (*dataptr) _kf_clear_key(kf, group, key); } STATIC void keyfile_handle_generic_uint(GKeyFile* kf, const gchar* group, const gchar* key, guint* dataptr, guint default_value) { g_assert(dataptr); if (g_key_file_has_key(kf, group, key, NULL)) { guint data = g_key_file_get_uint64(kf, group, key, NULL); if (data != default_value) *dataptr = data; _kf_clear_key(kf, group, key); } } STATIC void keyfile_handle_common(GKeyFile* kf, NetplanNetDefinition* nd, const gchar* group) { keyfile_handle_generic_uint(kf, group, "mtu", &nd->mtubytes, NETPLAN_MTU_UNSPEC); keyfile_handle_generic_str(kf, group, "mac-address", &nd->match.mac); if (nd->match.mac) nd->has_match = TRUE; } STATIC void keyfile_handle_bridge_uint(GKeyFile* kf, const gchar* key, NetplanNetDefinition* nd, char** dataptr) { if (g_key_file_get_uint64(kf, "bridge", key, NULL)) { nd->custom_bridging = TRUE; *dataptr = g_strdup_printf("%"G_GUINT64_FORMAT, g_key_file_get_uint64(kf, "bridge", key, NULL)); _kf_clear_key(kf, "bridge", key); } } STATIC void keyfile_handle_cloned_mac_address(GKeyFile *kf, NetplanNetDefinition* nd, const gchar* group) { g_autofree gchar* mac = g_key_file_get_string(kf, group, "cloned-mac-address", NULL); if (!mac) return; nd->set_mac = g_strdup(mac); _kf_clear_key(kf, group, "cloned-mac-address"); } STATIC void parse_addresses(GKeyFile* kf, const gchar* group, GArray** ip_arr) { g_assert(ip_arr); if (kf_matches(kf, group, "method", "manual")) { gboolean unhandled_data = FALSE; gchar *key = NULL; gchar *kf_value = NULL; gchar **split = NULL; for (unsigned i = 1;; ++i) { key = g_strdup_printf("address%u", i); kf_value = g_key_file_get_string(kf, group, key, NULL); if (!kf_value) { g_free(key); break; } if (!*ip_arr) *ip_arr = g_array_new(FALSE, FALSE, sizeof(char*)); split = g_strsplit(kf_value, ",", 2); g_free(kf_value); /* Append "address/prefix" */ if (split[0]) { /* no need to free 's', this will stay in the netdef */ gchar* s = g_strdup(split[0]); g_array_append_val(*ip_arr, s); } if (!split[1]) _kf_clear_key(kf, group, key); else /* XXX: how to handle additional values (like "gateway") in split[n]? */ unhandled_data = TRUE; g_strfreev(split); g_free(key); } /* clear keyfile once all data was handled */ if (!unhandled_data) _kf_clear_key(kf, group, "method"); } } STATIC void parse_routes(GKeyFile* kf, const gchar* group, GArray** routes_arr) { g_assert(routes_arr); NetplanIPRoute *route = NULL; gchar **split = NULL; for (unsigned i = 1;; ++i) { gboolean unhandled_data = FALSE; g_autofree gchar* key = g_strdup_printf("route%u", i); g_autofree gchar* kf_value = g_key_file_get_string(kf, group, key, NULL); g_autofree gchar* options_key = g_strdup_printf("route%u_options", i); g_autofree gchar* options_kf_value = g_key_file_get_string(kf, group, options_key, NULL); if (!kf_value) break; if (!*routes_arr) *routes_arr = g_array_new(FALSE, TRUE, sizeof(NetplanIPRoute*)); route = g_new0(NetplanIPRoute, 1); route->type = g_strdup("unicast"); route->family = -1; /* 0 is a valid family ID */ route->metric = NETPLAN_METRIC_UNSPEC; /* 0 is a valid metric */ g_debug("%s: adding new route (kf)", key); if (g_strcmp0(group, "ipv4") == 0) route->family = AF_INET; else if (g_strcmp0(group, "ipv6") == 0) route->family = AF_INET6; split = g_strsplit(kf_value, ",", 3); /* Append "to" (address/prefix) */ if (split[0]) route->to = g_strdup(split[0]); //no need to free, will stay in netdef /* Append gateway/via IP */ if (split[0] && split[1] && g_strcmp0(split[1], get_unspecified_address(route->family)) != 0 && g_strcmp0(split[1], "") != 0) { route->scope = g_strdup("global"); route->via = g_strdup(split[1]); //no need to free, will stay in netdef } else { /* If the gateway (via) is unspecified, it means that this route is * only valid on the local network (see nm-keyfile.c -> * read_one_ip_address_or_route()), e.g.: * ip route add NETWORK dev DEV [metric METRIC] */ route->scope = g_strdup("link"); } /* Append metric */ if (split[0] && split[1] && split[2] && strtoul(split[2], NULL, 10) != NETPLAN_METRIC_UNSPEC) route->metric = strtoul(split[2], NULL, 10); g_strfreev(split); /* Parse route options */ if (options_kf_value) { g_debug("%s: adding new route_options (kf)", options_key); split = g_strsplit(options_kf_value, ",", -1); for (unsigned i = 0; split[i]; ++i) { g_debug("processing route_option: %s", split[i]); gchar **kv = g_strsplit(split[i], "=", 2); if (g_strcmp0(kv[0], "onlink") == 0) route->onlink = (g_strcmp0(kv[1], "true") == 0); else if (g_strcmp0(kv[0], "initrwnd") == 0) route->advertised_receive_window = strtoul(kv[1], NULL, 10); else if (g_strcmp0(kv[0], "initcwnd") == 0) route->congestion_window = strtoul(kv[1], NULL, 10); else if (g_strcmp0(kv[0], "mtu") == 0) route->mtubytes = strtoul(kv[1], NULL, 10); else if (g_strcmp0(kv[0], "table") == 0) route->table = strtoul(kv[1], NULL, 10); else if (g_strcmp0(kv[0], "src") == 0) route->from = g_strdup(kv[1]); //no need to free, will stay in netdef else unhandled_data = TRUE; g_strfreev(kv); } g_strfreev(split); if (!unhandled_data) _kf_clear_key(kf, group, options_key); } /* Add route to array, clear keyfile */ g_array_append_val(*routes_arr, route); if (!unhandled_data) _kf_clear_key(kf, group, key); } } STATIC void parse_dhcp_overrides(GKeyFile* kf, const gchar* group, NetplanDHCPOverrides* dataptr) { g_assert(dataptr); if ( g_key_file_get_boolean(kf, group, "ignore-auto-routes", NULL) && g_key_file_get_boolean(kf, group, "never-default", NULL)) { (*dataptr).use_routes = FALSE; _kf_clear_key(kf, group, "ignore-auto-routes"); _kf_clear_key(kf, group, "never-default"); } keyfile_handle_generic_uint(kf, group, "route-metric", &(*dataptr).metric, NETPLAN_METRIC_UNSPEC); } /* static void parse_search_domains(GKeyFile* kf, const gchar* group, GArray** domains_arr) { // Keep "dns-search" as fallback/passthrough, as netplan cannot // differentiate between ipv4.dns-search and ipv6.dns-search g_assert(domains_arr); gsize len = 0; gchar **split = g_key_file_get_string_list(kf, group, "dns-search", &len, NULL); if (split) { if (len == 0) { //do not clear "dns-search", keep as fallback //_kf_clear_key(kf, group, "dns-search"); return; } if (!*domains_arr) *domains_arr = g_array_new(FALSE, FALSE, sizeof(char*)); for(unsigned i = 0; split[i]; ++i) { char* s = g_strdup(split[i]); //no need to free, will stay in netdef g_array_append_val(*domains_arr, s); } //do not clear "dns-search", keep as fallback //_kf_clear_key(kf, group, "dns-search"); g_strfreev(split); } } */ STATIC void parse_nameservers(GKeyFile* kf, const gchar* group, GArray** nameserver_arr) { g_assert(nameserver_arr); gchar **split = g_key_file_get_string_list(kf, group, "dns", NULL, NULL); if (split) { if (!*nameserver_arr) *nameserver_arr = g_array_new(FALSE, FALSE, sizeof(char*)); for(unsigned i = 0; split[i]; ++i) { if (strlen(split[i]) > 0) { gchar* s = g_strdup(split[i]); //no need to free, will stay in netdef g_array_append_val(*nameserver_arr, s); } } _kf_clear_key(kf, group, "dns"); g_strfreev(split); } } STATIC void parse_dot1x_auth(GKeyFile* kf, NetplanAuthenticationSettings* auth) { g_assert(auth); g_autofree gchar* method = g_key_file_get_string(kf, "802-1x", "eap", NULL); if (method && g_strcmp0(method, "") != 0) { gchar** split = g_strsplit(method, ";", 2); gchar* first_method = split[0]; if (g_strcmp0(first_method, "tls") == 0) { auth->eap_method = NETPLAN_AUTH_EAP_TLS; } else if (g_strcmp0(first_method, "peap") == 0) { auth->eap_method = NETPLAN_AUTH_EAP_PEAP; } else if (g_strcmp0(first_method, "ttls") == 0) { auth->eap_method = NETPLAN_AUTH_EAP_TTLS; } else if (g_strcmp0(first_method, "leap") == 0) { auth->eap_method = NETPLAN_AUTH_EAP_LEAP; } else if (g_strcmp0(first_method, "pwd") == 0) { auth->eap_method = NETPLAN_AUTH_EAP_PWD; } else { auth->eap_method = NETPLAN_AUTH_EAP_UNKNOWN; } /* If "method" (which is a list separated by ";") has more than one value, * we keep the key so it will also be written as a passthrough key. * That's required because Network Manager accepts multiple methods * but Netplan accepts only one. * * TODO: eap_method needs to be fixed to store multiple methods. * * If at this point the eap_method is still UNKNOWN we also keep the property because * it's probably a setting we still don't support. */ if (auth->eap_method != NETPLAN_AUTH_EAP_UNKNOWN && (split[1] == NULL || !g_strcmp0(split[1], ""))) _kf_clear_key(kf, "802-1x", "eap"); g_strfreev(split); } keyfile_handle_generic_str(kf, "802-1x", "identity", &auth->identity); keyfile_handle_generic_str(kf, "802-1x", "anonymous-identity", &auth->anonymous_identity); keyfile_handle_generic_str(kf, "802-1x", "password", &auth->password); keyfile_handle_generic_str(kf, "802-1x", "ca-cert", &auth->ca_certificate); keyfile_handle_generic_str(kf, "802-1x", "client-cert", &auth->client_certificate); keyfile_handle_generic_str(kf, "802-1x", "private-key", &auth->client_key); keyfile_handle_generic_str(kf, "802-1x", "private-key-password", &auth->client_key_password); keyfile_handle_generic_str(kf, "802-1x", "phase2-auth", &auth->phase2_auth); } STATIC void parse_bond_arp_ip_targets(GKeyFile* kf, GArray **targets_arr) { g_assert(targets_arr); g_autofree gchar *v = g_key_file_get_string(kf, "bond", "arp_ip_target", NULL); if (v) { gchar** split = g_strsplit(v, ",", -1); for (unsigned i = 0; split[i]; ++i) { if (!*targets_arr) *targets_arr = g_array_new(FALSE, FALSE, sizeof(char *)); gchar *s = g_strdup(split[i]); g_array_append_val(*targets_arr, s); } _kf_clear_key(kf, "bond", "arp_ip_target"); g_strfreev(split); } } /* Read the key-value pairs from the keyfile and pass them through to a map */ STATIC void read_passthrough(GKeyFile* kf, GData** list) { gchar **groups = NULL; gchar **keys = NULL; gchar *group_key = NULL; gchar *value = NULL; gsize klen = 0; gsize glen = 0; if (!*list) g_datalist_init(list); groups = g_key_file_get_groups(kf, &glen); if (groups) { for (unsigned i = 0; i < glen; ++i) { klen = 0; keys = g_key_file_get_keys(kf, groups[i], &klen, NULL); if (klen == 0) { /* empty group */ g_datalist_set_data_full(list, g_strconcat(groups[i], ".", NETPLAN_NM_EMPTY_GROUP, NULL), g_strdup(""), g_free); continue; } for (unsigned j = 0; j < klen; ++j) { value = g_key_file_get_string(kf, groups[i], keys[j], NULL); if (!value) { // LCOV_EXCL_START g_warning("netplan: Keyfile: cannot read value of %s.%s", groups[i], keys[j]); continue; // LCOV_EXCL_STOP } group_key = g_strconcat(groups[i], ".", keys[j], NULL); g_datalist_set_data_full(list, group_key, value, g_free); g_free(group_key); } g_strfreev(keys); } g_strfreev(groups); } } /* * Network Manager differentiates Wireguard (connection.type=wireguard), * VXLAN (connection.type=vxlan) and all the other types of tunnels (connection.type=ip-tunnel). * * Each of these three classes have different requirements so we handle them separately * in this function. */ STATIC void parse_tunnels(GKeyFile* kf, NetplanNetDefinition* nd) { /* Handle wireguard tunnel */ if (nd->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) { /* Reading the private key */ nd->tunnel.private_key = g_key_file_get_string(kf, "wireguard", "private-key", NULL); _kf_clear_key(kf, "wireguard", "private-key"); /* Reading the listen port */ nd->tunnel.port = g_key_file_get_uint64(kf, "wireguard", "listen-port", NULL); _kf_clear_key(kf, "wireguard", "listen-port"); nd->tunnel_private_key_flags = g_key_file_get_integer(kf, "wireguard", "private-key-flags", NULL); _kf_clear_key(kf, "wireguard", "private-key-flags"); gchar** keyfile_groups = g_key_file_get_groups(kf, NULL); /* Handling peers * Network Manager creates a keyfile group for each Wireguard peer. * The group name has the form [wireguard-peer.] so, * in order to read the peer's public key we need to split up the group name * and read its second component. * */ for (int i = 0; keyfile_groups[i] != NULL; i++) { gchar* group = keyfile_groups[i]; if (g_str_has_prefix(group, "wireguard-peer.")) { gchar** peer_split = g_strsplit(group, ".", 2); if (!is_wireguard_key(peer_split[1])) { g_warning("Wireguard peer's name is malformed: %s", group); g_strfreev(peer_split); continue; } if (!nd->wireguard_peers) nd->wireguard_peers = g_array_new(FALSE, FALSE, sizeof(NetplanWireguardPeer*)); NetplanWireguardPeer* wireguard_peer = g_new0(NetplanWireguardPeer, 1); wireguard_peer->public_key = g_strdup(peer_split[1]); g_strfreev(peer_split); /* Handle allowed-ips */ gchar* allowed_ips_str = g_key_file_get_string(kf, group, "allowed-ips", NULL); if (allowed_ips_str) { wireguard_peer->allowed_ips = g_array_new(FALSE, FALSE, sizeof(NetplanAddressOptions*)); gchar** allowed_ips_split = g_strsplit(allowed_ips_str, ";", 0); for (int i = 0; allowed_ips_split[i] != NULL; i++) { gchar* ip = allowed_ips_split[i]; if (g_strcmp0(ip, "")) { gchar* address = NULL; /* * NM doesn't care if the prefix was omitted. * Even though the WG manual says it requires the prefix, * if it's omitted in its config file it will default to /32 for IPv4 * and /128 for IPv6 so we should do the same here and append a /32 or /128 * if it's not present, otherwise we will generate a YAML that will fail validation. */ if (!g_strrstr(ip, "/")) { if (is_ip4_address(ip)) address = g_strdup_printf("%s/32", ip); else address = g_strdup_printf("%s/128", ip); } else address = g_strdup(ip); g_array_append_val(wireguard_peer->allowed_ips, address); } } g_free(allowed_ips_str); g_strfreev(allowed_ips_split); _kf_clear_key(kf, group, "allowed-ips"); } /* Handle endpoint */ gchar* endpoint = g_key_file_get_string(kf, group, "endpoint", NULL); if (endpoint && g_strcmp0(endpoint, "")) { /* Only set the endpoint if it's not NULL nor an empty string */ wireguard_peer->endpoint = endpoint; } _kf_clear_key(kf, group, "endpoint"); g_array_append_val(nd->wireguard_peers, wireguard_peer); } } g_strfreev(keyfile_groups); } else if (nd->tunnel.mode == NETPLAN_TUNNEL_MODE_VXLAN) { /* Handle vxlan tunnel */ nd->vxlan = g_new0(NetplanVxlan, 1); reset_vxlan(nd->vxlan); /* Reading the VXLAN ID*/ nd->vxlan->vni = g_key_file_get_integer(kf, "vxlan", "id", NULL); _kf_clear_key(kf, "vxlan", "id"); nd->tunnel.local_ip = g_key_file_get_string(kf, "vxlan", "local", NULL); _kf_clear_key(kf, "vxlan", "local"); nd->tunnel.remote_ip = g_key_file_get_string(kf, "vxlan", "remote", NULL); _kf_clear_key(kf, "vxlan", "remote"); } else { /* Handle all the other types of tunnel */ nd->tunnel.mode = g_key_file_get_integer(kf, "ip-tunnel", "mode", NULL); /* We don't want to automatically accept new types of tunnels introduced by Network Manager */ if (nd->tunnel.mode >= NETPLAN_TUNNEL_MODE_NM_MAX) { nd->tunnel.mode = NETPLAN_TUNNEL_MODE_UNKNOWN; return; } _kf_clear_key(kf, "ip-tunnel", "mode"); nd->tunnel.local_ip = g_key_file_get_string(kf, "ip-tunnel", "local", NULL); _kf_clear_key(kf, "ip-tunnel", "local"); nd->tunnel.remote_ip = g_key_file_get_string(kf, "ip-tunnel", "remote", NULL); _kf_clear_key(kf, "ip-tunnel", "remote"); } } gboolean netplan_parser_load_keyfile(NetplanParser* npp, const char* filename, GError** error) { g_autofree gchar *nd_id = NULL; g_autofree gchar *uuid = NULL; g_autofree gchar *type = NULL; g_autofree gchar* wifi_mode = NULL; g_autofree gchar* ssid = NULL; g_autofree gchar* netdef_id = NULL; ssize_t netdef_id_size = 0; gchar *tmp_str = NULL; gint pmf = 0; NetplanNetDefinition* nd = NULL; NetplanWifiAccessPoint* ap = NULL; g_autoptr(GKeyFile) kf = g_key_file_new(); NetplanDefType nd_type = NETPLAN_DEF_TYPE_NONE; if (!g_key_file_load_from_file(kf, filename, G_KEY_FILE_NONE, error)) { g_warning("netplan: cannot load keyfile"); return FALSE; } ssid = g_key_file_get_string(kf, "wifi", "ssid", NULL); if (!ssid) ssid = g_key_file_get_string(kf, "802-11-wireless", "ssid", NULL); netdef_id = g_malloc0(strlen(filename)); netdef_id_size = netplan_get_id_from_nm_filepath(filename, ssid, netdef_id, strlen(filename)); uuid = g_key_file_get_string(kf, "connection", "uuid", NULL); if (!uuid) { const char* msg = "netplan: Keyfile: cannot find connection.uuid"; g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "%s", msg); g_warning("%s", msg); return FALSE; } type = g_key_file_get_string(kf, "connection", "type", NULL); if (!type) { const char* msg = "netplan: Keyfile: cannot find connection.type"; g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "%s", msg); g_warning("%s", msg); return FALSE; } nd_type = type_from_str(type); tmp_str = g_key_file_get_string(kf, "connection", "interface-name", NULL); /* Use previously existing netdef IDs, if available, to override connections * Else: generate a "NM-" ID */ if (netdef_id_size > 0) { nd_id = g_strdup(netdef_id); if (g_strcmp0(netdef_id, tmp_str) == 0) _kf_clear_key(kf, "connection", "interface-name"); } else if (tmp_str && nd_type >= NETPLAN_DEF_TYPE_VIRTUAL && nd_type < NETPLAN_DEF_TYPE_NM) { /* netdef ID equals "interface-name" for virtual devices (bridge/bond/...) */ nd_id = g_strdup(tmp_str); _kf_clear_key(kf, "connection", "interface-name"); } else nd_id = g_strconcat("NM-", uuid, NULL); g_free(tmp_str); nd = netplan_netdef_new(npp, nd_id, nd_type, NETPLAN_BACKEND_NM); /* Handle uuid & NM name/id */ nd->backend_settings.uuid = g_strdup(uuid); _kf_clear_key(kf, "connection", "uuid"); nd->backend_settings.name = g_key_file_get_string(kf, "connection", "id", NULL); if (nd->backend_settings.name) _kf_clear_key(kf, "connection", "id"); if (nd_type == NETPLAN_DEF_TYPE_NM) goto only_passthrough; //do not try to handle any keys for connections types unknown to netplan /* Handle some differing NM/netplan defaults */ tmp_str = g_key_file_get_string(kf, "ipv6", "method", NULL); if ( g_key_file_has_group(kf, "ipv6") && g_strcmp0(tmp_str, "ignore") != 0 && !g_key_file_has_key(kf, "ipv6", "ip6-privacy", NULL)) { /* put NM's default into passthrough, as this is not currently supported by netplan */ g_key_file_set_integer(kf, "ipv6", "ip6-privacy", -1); } g_free(tmp_str); /* Handle tunnels */ if (nd_type == NETPLAN_DEF_TYPE_TUNNEL) { nd->tunnel.mode = tunnel_mode_from_str(type); parse_tunnels(kf, nd); } /* Handle veths */ if (nd_type == NETPLAN_DEF_TYPE_VETH) { g_autofree gchar* veth_peer = g_key_file_get_string(kf, "veth", "peer", NULL); if (!veth_peer) { g_warning("netplan: Keyfile: cannot find veth.peer"); return FALSE; } else { /* * Generate a placeholder interface to be the VETH's peer. * It's required because Network Manager allows the creation of * VETHs connections with a non-existing peer. */ nd->veth_peer_link = netplan_netdef_new(npp, veth_peer, NETPLAN_DEF_TYPE_NM_PLACEHOLDER_, NETPLAN_BACKEND_NM); _kf_clear_key(kf, "veth", "peer"); } } /* Handle VRFs */ if (nd_type == NETPLAN_DEF_TYPE_VRF) { if (g_key_file_has_key(kf, "vrf", "table", NULL)) { nd->vrf_table = g_key_file_get_uint64(kf, "vrf", "table", NULL); _kf_clear_key(kf, "vrf", "table"); } } /* remove supported values from passthrough, which have been handled */ if ( nd_type == NETPLAN_DEF_TYPE_ETHERNET || nd_type == NETPLAN_DEF_TYPE_WIFI || nd_type == NETPLAN_DEF_TYPE_MODEM || nd_type == NETPLAN_DEF_TYPE_BRIDGE || nd_type == NETPLAN_DEF_TYPE_BOND || nd_type == NETPLAN_DEF_TYPE_DUMMY /* wokeignore:rule=dummy */ || nd_type == NETPLAN_DEF_TYPE_VLAN || nd_type == NETPLAN_DEF_TYPE_VETH || nd_type == NETPLAN_DEF_TYPE_VRF || (nd_type == NETPLAN_DEF_TYPE_TUNNEL && nd->tunnel.mode != NETPLAN_TUNNEL_MODE_UNKNOWN)) _kf_clear_key(kf, "connection", "type"); /* Handle match: Netplan usually defines a connection per interface, while * NM connection profiles are usually applied to any interface of matching * type (like wifi/ethernet/...). */ if (nd->type < NETPLAN_DEF_TYPE_VIRTUAL) { nd->match.original_name = g_key_file_get_string(kf, "connection", "interface-name", NULL); if (nd->match.original_name) _kf_clear_key(kf, "connection", "interface-name"); /* Set match, even if it is empty, so the NM renderer will not force * the netdef ID as interface-name */ nd->has_match = TRUE; } /* DHCPv4/v6 */ set_true_on_match(kf, "ipv4", "method", "auto", &nd->dhcp4); set_true_on_match(kf, "ipv6", "method", "auto", &nd->dhcp6); parse_dhcp_overrides(kf, "ipv4", &nd->dhcp4_overrides); parse_dhcp_overrides(kf, "ipv6", &nd->dhcp6_overrides); /* Manual IPv4/6 addresses */ parse_addresses(kf, "ipv4", &nd->ip4_addresses); parse_addresses(kf, "ipv6", &nd->ip6_addresses); /* Default gateways */ keyfile_handle_generic_str(kf, "ipv4", "gateway", &nd->gateway4); keyfile_handle_generic_str(kf, "ipv6", "gateway", &nd->gateway6); /* Routes */ parse_routes(kf, "ipv4", &nd->routes); parse_routes(kf, "ipv6", &nd->routes); /* DNS: XXX: How to differentiate ip4/ip6 search_domains? parse_search_domains(kf, "ipv4", &nd->search_domains); parse_search_domains(kf, "ipv6", &nd->search_domains); */ parse_nameservers(kf, "ipv4", &nd->ip4_nameservers); parse_nameservers(kf, "ipv6", &nd->ip6_nameservers); /* IP6 addr-gen * Different than suggested by the docs, NM stores 'addr-gen-mode' as string */ tmp_str = g_key_file_get_string(kf, "ipv6", "addr-gen-mode", NULL); if (tmp_str) { if (g_strcmp0(tmp_str, "stable-privacy") == 0) { nd->ip6_addr_gen_mode = NETPLAN_ADDRGEN_STABLEPRIVACY; _kf_clear_key(kf, "ipv6", "addr-gen-mode"); } else if (g_strcmp0(tmp_str, "eui64") == 0) { nd->ip6_addr_gen_mode = NETPLAN_ADDRGEN_EUI64; _kf_clear_key(kf, "ipv6", "addr-gen-mode"); } } g_free(tmp_str); keyfile_handle_generic_str(kf, "ipv6", "token", &nd->ip6_addr_gen_token); /* ip6-privacy is not fully supported, NM supports additional modes, like -1 or 1 * handle known modes, but keep any unsupported "ip6-privacy" value in passthrough */ if (g_key_file_has_group(kf, "ipv6")) { if (g_key_file_has_key(kf, "ipv6", "ip6-privacy", NULL)) { int ip6_privacy = g_key_file_get_integer(kf, "ipv6", "ip6-privacy", NULL); if (ip6_privacy == 0) { nd->ip6_privacy = FALSE; _kf_clear_key(kf, "ipv6", "ip6-privacy"); } else if (ip6_privacy == 2) { nd->ip6_privacy = TRUE; _kf_clear_key(kf, "ipv6", "ip6-privacy"); } } } /* Modem parameters * NM differentiates between GSM and CDMA connections, while netplan * combines them as "modems". We need to parse a basic set of parameters * to enable the generator (in nm.c) to detect GSM vs CDMA connections, * using its modem_is_gsm() util. */ keyfile_handle_generic_bool(kf, "gsm", "auto-config", &nd->modem_params.auto_config); keyfile_handle_generic_str(kf, "gsm", "apn", &nd->modem_params.apn); keyfile_handle_generic_str(kf, "gsm", "device-id", &nd->modem_params.device_id); keyfile_handle_generic_str(kf, "gsm", "network-id", &nd->modem_params.network_id); keyfile_handle_generic_str(kf, "gsm", "pin", &nd->modem_params.pin); keyfile_handle_generic_str(kf, "gsm", "sim-id", &nd->modem_params.sim_id); keyfile_handle_generic_str(kf, "gsm", "sim-operator-id", &nd->modem_params.sim_operator_id); /* GSM & CDMA */ keyfile_handle_generic_uint(kf, "cdma", "mtu", &nd->mtubytes, NETPLAN_MTU_UNSPEC); keyfile_handle_generic_uint(kf, "gsm", "mtu", &nd->mtubytes, NETPLAN_MTU_UNSPEC); keyfile_handle_generic_str(kf, "gsm", "number", &nd->modem_params.number); if (!nd->modem_params.number) keyfile_handle_generic_str(kf, "cdma", "number", &nd->modem_params.number); keyfile_handle_generic_str(kf, "gsm", "password", &nd->modem_params.password); if (!nd->modem_params.password) keyfile_handle_generic_str(kf, "cdma", "password", &nd->modem_params.password); keyfile_handle_generic_str(kf, "gsm", "username", &nd->modem_params.username); if (!nd->modem_params.username) keyfile_handle_generic_str(kf, "cdma", "username", &nd->modem_params.username); /* Ethernets */ if (g_key_file_has_group(kf, "ethernet")) { /* wake-on-lan, do not clear passthrough as we do not fully support this setting */ if (!g_key_file_has_key(kf, "ethernet", "wake-on-lan", NULL)) { /* apply the default only to actual ethernet devices */ if (nd_type == NETPLAN_DEF_TYPE_ETHERNET) nd->wake_on_lan = TRUE; //NM's default is "1" } else { guint value = g_key_file_get_uint64(kf, "ethernet", "wake-on-lan", NULL); //XXX: fix delta between options in NM (0x1, 0x2, 0x4, ...) and netplan (bool) nd->wake_on_lan = value > 0; // netplan only knows about "off" or "on" if (value == 0) _kf_clear_key(kf, "ethernet", "wake-on-lan"); // value "off" is supported } keyfile_handle_common(kf, nd, "ethernet"); keyfile_handle_cloned_mac_address(kf, nd, "ethernet"); } /* Wifis */ if (g_key_file_has_group(kf, "wifi")) { if (g_key_file_get_uint64(kf, "wifi", "wake-on-wlan", NULL)) { nd->wowlan = g_key_file_get_uint64(kf, "wifi", "wake-on-wlan", NULL); _kf_clear_key(kf, "wifi", "wake-on-wlan"); } else { nd->wowlan = NETPLAN_WIFI_WOWLAN_DEFAULT; } keyfile_handle_common(kf, nd, "wifi"); keyfile_handle_cloned_mac_address(kf, nd, "wifi"); } /* Cleanup some implicit keys */ tmp_str = g_key_file_get_string(kf, "ipv6", "method", NULL); if (tmp_str && g_strcmp0(tmp_str, "ignore") == 0 && !(nd->dhcp6 || nd->ip6_addresses || nd->gateway6 || nd->ip6_nameservers || nd->ip6_addr_gen_mode)) _kf_clear_key(kf, "ipv6", "method"); g_free(tmp_str); tmp_str = g_key_file_get_string(kf, "ipv4", "method", NULL); if (tmp_str && g_strcmp0(tmp_str, "link-local") == 0 && !(nd->dhcp4 || nd->ip4_addresses || nd->gateway4 || nd->ip4_nameservers)) _kf_clear_key(kf, "ipv4", "method"); g_free(tmp_str); /* Handling VLANs */ if (nd_type == NETPLAN_DEF_TYPE_VLAN) { keyfile_handle_generic_uint(kf, "vlan", "id", &nd->vlan_id, G_MAXUINT); g_autofree gchar* parent = g_key_file_get_string(kf, "vlan", "parent", NULL); if (parent) { /* * Generate a placeholder interface to be the VLAN's parent. * It's required because Network Manager allows the creation of * VLAN connections with non-existing parent interfaces. */ nd->vlan_link = netplan_netdef_new(npp, parent, NETPLAN_DEF_TYPE_NM_PLACEHOLDER_, NETPLAN_BACKEND_NM); _kf_clear_key(kf, "vlan", "parent"); } } /* Bridge: XXX: find a way to parse the bridge-port.priority & bridge-port.path-cost values */ keyfile_handle_generic_uint(kf, "bridge", "priority", &nd->bridge_params.priority, 0); if (nd->bridge_params.priority) nd->custom_bridging = TRUE; keyfile_handle_bridge_uint(kf, "ageing-time", nd, &nd->bridge_params.ageing_time); keyfile_handle_bridge_uint(kf, "hello-time", nd, &nd->bridge_params.hello_time); keyfile_handle_bridge_uint(kf, "forward-delay", nd, &nd->bridge_params.forward_delay); keyfile_handle_bridge_uint(kf, "max-age", nd, &nd->bridge_params.max_age); /* STP needs to be handled last, for its different default value in custom_bridging mode */ if (g_key_file_has_key(kf, "bridge", "stp", NULL)) { nd->custom_bridging = TRUE; keyfile_handle_generic_bool(kf, "bridge", "stp", &nd->bridge_params.stp); } else if(nd->custom_bridging) { nd->bridge_params.stp = TRUE; //set default value if not specified otherwise } /* Bonds */ keyfile_handle_generic_str(kf, "bond", "mode", &nd->bond_params.mode); keyfile_handle_generic_str(kf, "bond", "lacp_rate", &nd->bond_params.lacp_rate); keyfile_handle_generic_str(kf, "bond", "miimon", &nd->bond_params.monitor_interval); keyfile_handle_generic_str(kf, "bond", "xmit_hash_policy", &nd->bond_params.transmit_hash_policy); keyfile_handle_generic_str(kf, "bond", "ad_select", &nd->bond_params.selection_logic); keyfile_handle_generic_str(kf, "bond", "arp_interval", &nd->bond_params.arp_interval); keyfile_handle_generic_str(kf, "bond", "arp_validate", &nd->bond_params.arp_validate); keyfile_handle_generic_str(kf, "bond", "arp_all_targets", &nd->bond_params.arp_all_targets); keyfile_handle_generic_str(kf, "bond", "updelay", &nd->bond_params.up_delay); keyfile_handle_generic_str(kf, "bond", "downdelay", &nd->bond_params.down_delay); keyfile_handle_generic_str(kf, "bond", "fail_over_mac", &nd->bond_params.fail_over_mac_policy); keyfile_handle_generic_str(kf, "bond", "primary_reselect", &nd->bond_params.primary_reselect_policy); keyfile_handle_generic_str(kf, "bond", "lp_interval", &nd->bond_params.learn_interval); keyfile_handle_generic_str(kf, "bond", "primary", &nd->bond_params.primary_member); keyfile_handle_generic_uint(kf, "bond", "min_links", &nd->bond_params.min_links, 0); keyfile_handle_generic_uint(kf, "bond", "resend_igmp", &nd->bond_params.resend_igmp, 0); keyfile_handle_generic_uint(kf, "bond", "packets_per_slave", &nd->bond_params.packets_per_member, 0); /* wokeignore:rule=slave */ keyfile_handle_generic_uint(kf, "bond", "num_grat_arp", &nd->bond_params.gratuitous_arp, 0); /* num_unsol_na might overwrite num_grat_arp, but we're fine if they are equal: * https://github.com/NetworkManager/NetworkManager/commit/42b0bef33c77a0921590b2697f077e8ea7805166 */ if (g_key_file_get_uint64(kf, "bond", "num_unsol_na", NULL) == nd->bond_params.gratuitous_arp) _kf_clear_key(kf, "bond", "num_unsol_na"); keyfile_handle_generic_bool(kf, "bond", "all_slaves_active", &nd->bond_params.all_members_active); /* wokeignore:rule=slave */ parse_bond_arp_ip_targets(kf, &nd->bond_params.arp_ip_targets); /* Special handling for WiFi "access-points:" mapping */ if (nd->type == NETPLAN_DEF_TYPE_WIFI) { ap = g_new0(NetplanWifiAccessPoint, 1); ap->ssid = g_key_file_get_string(kf, "wifi", "ssid", NULL); if (!ap->ssid) { const char* msg = "netplan: Keyfile: cannot find SSID for WiFi connection"; g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "%s", msg); g_warning("%s", msg); g_free(ap); return FALSE; } else _kf_clear_key(kf, "wifi", "ssid"); wifi_mode = g_key_file_get_string(kf, "wifi", "mode", NULL); if (wifi_mode) { ap->mode = ap_type_from_str(wifi_mode); if (ap->mode != NETPLAN_WIFI_MODE_OTHER) _kf_clear_key(kf, "wifi", "mode"); } tmp_str = g_key_file_get_string(kf, "ipv4", "method", NULL); if (tmp_str && g_strcmp0(tmp_str, "shared") == 0) { ap->mode = NETPLAN_WIFI_MODE_AP; _kf_clear_key(kf, "ipv4", "method"); } g_free(tmp_str); keyfile_handle_generic_bool(kf, "wifi", "hidden", &ap->hidden); keyfile_handle_generic_str(kf, "wifi", "bssid", &ap->bssid); /* Wifi band & channel */ tmp_str = g_key_file_get_string(kf, "wifi", "band", NULL); if (tmp_str && g_strcmp0(tmp_str, "a") == 0) { ap->band = NETPLAN_WIFI_BAND_5; _kf_clear_key(kf, "wifi", "band"); } else if (tmp_str && g_strcmp0(tmp_str, "bg") == 0) { ap->band = NETPLAN_WIFI_BAND_24; _kf_clear_key(kf, "wifi", "band"); } g_free(tmp_str); keyfile_handle_generic_uint(kf, "wifi", "channel", &ap->channel, 0); /* Wifi security */ tmp_str = g_key_file_get_string(kf, "wifi-security", "key-mgmt", NULL); if (tmp_str && g_strcmp0(tmp_str, "wpa-psk") == 0) { ap->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK; ap->has_auth = TRUE; _kf_clear_key(kf, "wifi-security", "key-mgmt"); } else if (tmp_str && g_strcmp0(tmp_str, "wpa-eap") == 0) { ap->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP; ap->has_auth = TRUE; _kf_clear_key(kf, "wifi-security", "key-mgmt"); } else if (tmp_str && g_strcmp0(tmp_str, "wpa-eap-suite-b-192") == 0) { ap->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSUITE_B_192; ap->has_auth = TRUE; _kf_clear_key(kf, "wifi-security", "key-mgmt"); } else if (tmp_str && g_strcmp0(tmp_str, "sae") == 0) { ap->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_SAE; ap->has_auth = TRUE; _kf_clear_key(kf, "wifi-security", "key-mgmt"); } else if (tmp_str && g_strcmp0(tmp_str, "ieee8021x") == 0) { ap->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_8021X; ap->has_auth = TRUE; _kf_clear_key(kf, "wifi-security", "key-mgmt"); } g_free(tmp_str); pmf = g_key_file_get_integer(kf, "wifi-security", "pmf", NULL); switch (pmf) { case 2: ap->auth.pmf_mode = NETPLAN_AUTH_PMF_MODE_OPTIONAL; _kf_clear_key(kf, "wifi-security", "pmf"); /* If pmf is set to 2 (optional) and the key management is EAP * we set it to EAPSHA256 so the correct method is emitted in the YAML */ if (ap->auth.key_management == NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP) ap->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSHA256; break; case 3: ap->auth.pmf_mode = NETPLAN_AUTH_PMF_MODE_REQUIRED; _kf_clear_key(kf, "wifi-security", "pmf"); break; default: break; } keyfile_handle_generic_str(kf, "wifi-security", "psk", &ap->auth.psk); if (ap->auth.psk) ap->has_auth = TRUE; parse_dot1x_auth(kf, &ap->auth); if (ap->auth.eap_method != NETPLAN_AUTH_EAP_NONE) ap->has_auth = TRUE; if (!nd->access_points) nd->access_points = g_hash_table_new(g_str_hash, g_str_equal); g_hash_table_insert(nd->access_points, ap->ssid, ap); /* Last: handle passthrough for everything left in the keyfile * Also, transfer backend_settings from netdef to AP */ ap->backend_settings.uuid = g_strdup(nd->backend_settings.uuid); ap->backend_settings.name = g_strdup(nd->backend_settings.name); /* No need to clear nm.uuid & nm.name from def->backend_settings, * as we have only one AP. */ read_passthrough(kf, &ap->backend_settings.passthrough); } else { only_passthrough: /* Last: handle passthrough for everything left in the keyfile */ read_passthrough(kf, &nd->backend_settings.passthrough); } /* validate definition-level conditions */ if (!npp->missing_id) npp->missing_id = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); if (!validate_netdef_grammar(npp, nd, error)) return FALSE; return TRUE; } netplan-1.0/src/parse.c000066400000000000000000004722241457004145200150750ustar00rootroot00000000000000/* * Copyright (C) 2016-2023 Canonical, Ltd. * Author: Martin Pitt * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include "parse.h" #include "names.h" #include "util-internal.h" #include "error.h" #include "validation.h" #define NETPLAN_VERSION_MIN 2 #define NETPLAN_VERSION_MAX 3 /* convenience macro to put the offset of a NetplanNetDefinition field into "void* data" */ #define access_point_offset(field) GUINT_TO_POINTER(offsetof(NetplanWifiAccessPoint, field)) #define addr_option_offset(field) GUINT_TO_POINTER(offsetof(NetplanAddressOptions, field)) #define auth_offset(field) GUINT_TO_POINTER(offsetof(NetplanAuthenticationSettings, field)) #define ip_rule_offset(field) GUINT_TO_POINTER(offsetof(NetplanIPRule, field)) #define netdef_offset(field) GUINT_TO_POINTER(offsetof(NetplanNetDefinition, field)) #define ovs_settings_offset(field) GUINT_TO_POINTER(offsetof(NetplanOVSSettings, field)) #define route_offset(field) GUINT_TO_POINTER(offsetof(NetplanIPRoute, field)) #define wireguard_peer_offset(field) GUINT_TO_POINTER(offsetof(NetplanWireguardPeer, field)) #define vxlan_offset(field) GUINT_TO_POINTER(offsetof(NetplanVxlan, field)) /* convenience macro to avoid strdup'ing a string into a field if it's already set. */ #define set_str_if_null(dst, src) { if (dst) {\ g_assert_cmpstr(src, ==, dst); \ } else { \ dst = g_strdup(src); \ } } STATIC gboolean insert_kv_into_hash(void *key, void *value, void *hash); /** * Load YAML file into a yaml_document_t. * * @input_fd: the file descriptor pointing to the YAML source file * @doc: the output document structure * * Returns: TRUE on success, FALSE if the document is malformed; @error gets set then. */ STATIC gboolean load_yaml_from_fd(int input_fd, yaml_document_t* doc, GError** error) { int in_dup = -1; FILE* fyaml = NULL; yaml_parser_t parser; gboolean ret = TRUE; in_dup = dup(input_fd); if (in_dup < 0) goto file_error; // LCOV_EXCL_LINE fyaml = fdopen(in_dup, "r"); if (!fyaml) goto file_error; // LCOV_EXCL_LINE yaml_parser_initialize(&parser); yaml_parser_set_input_file(&parser, fyaml); if (!yaml_parser_load(&parser, doc)) { ret = parser_error(&parser, NULL, error); } yaml_parser_delete(&parser); fclose(fyaml); return ret; // LCOV_EXCL_START file_error: g_set_error(error, NETPLAN_FILE_ERROR, errno, "Error when opening FD %d: %m", input_fd); if (in_dup >= 0) close(in_dup); return FALSE; // LCOV_EXCL_STOP } /** * Load YAML file name into a yaml_document_t. * * @yaml: file path to the YAML source file * @doc: the output document structure * * Returns: TRUE on success, FALSE if the document is malformed; @error gets set then. */ STATIC gboolean load_yaml(const char* yaml, yaml_document_t* doc, GError** error) { FILE* fyaml = NULL; yaml_parser_t parser; gboolean ret = TRUE; fyaml = g_fopen(yaml, "r"); if (!fyaml) { // LCOV_EXCL_START g_set_error(error, NETPLAN_FILE_ERROR, errno, "Cannot open %s: %m", yaml); return FALSE; } // LCOV_EXCL_STOP yaml_parser_initialize(&parser); yaml_parser_set_input_file(&parser, fyaml); if (!yaml_parser_load(&parser, doc)) { ret = parser_error(&parser, yaml, error); } yaml_parser_delete(&parser); fclose(fyaml); return ret; } #define YAML_VARIABLE_NODE YAML_NO_NODE /** * Raise a GError about a type mismatch and return FALSE. */ STATIC gboolean assert_type_fn(const NetplanParser* npp, yaml_node_t* node, yaml_node_type_t expected_type, GError** error) { if (node->type == expected_type) return TRUE; switch (expected_type) { case YAML_VARIABLE_NODE: /* Special case, defer coherence checking to the next handlers */ return TRUE; break; case YAML_SCALAR_NODE: yaml_error(npp, node, error, "expected scalar"); break; case YAML_SEQUENCE_NODE: yaml_error(npp, node, error, "expected sequence"); break; case YAML_MAPPING_NODE: yaml_error(npp, node, error, "expected mapping (check indentation)"); break; // LCOV_EXCL_START default: g_assert_not_reached(); // LCOV_EXCL_STOP } return FALSE; } #define assert_type(ctx,n,t) { if (!assert_type_fn(ctx,n,t,error)) return FALSE; } static inline const char* scalar(const yaml_node_t* node) { return (const char*) node->data.scalar.value; } STATIC void add_missing_node(NetplanParser *npp, const yaml_node_t* node) { NetplanMissingNode* missing; /* Let's capture the current netdef we were playing with along with the * actual yaml_node_t that errors (that is an identifier not previously * seen by the compiler). We can use it later to write an sensible error * message and point the user in the right direction. */ missing = g_new0(NetplanMissingNode, 1); missing->netdef_id = npp->current.netdef->id; missing->node = node; g_debug("recording missing yaml_node_t %s", scalar(node)); g_hash_table_insert(npp->missing_id, (gpointer)scalar(node), missing); } /** * Check that node contains a valid ID/interface name. Raise GError if not. */ STATIC gboolean assert_valid_id(const NetplanParser* npp, yaml_node_t* node, GError** error) { static regex_t re; static gboolean re_inited = FALSE; assert_type(npp, node, YAML_SCALAR_NODE); if (!re_inited) { g_assert(regcomp(&re, "^[[:alnum:][:punct:]]+$", REG_EXTENDED|REG_NOSUB) == 0); re_inited = TRUE; } if (regexec(&re, scalar(node), 0, NULL, 0) != 0) return yaml_error(npp, node, error, "Invalid name '%s'", scalar(node)); return TRUE; } NetplanNetDefinition* netplan_netdef_new(NetplanParser *npp, const char* id, NetplanDefType type, NetplanBackend backend) { /* create new network definition */ NetplanNetDefinition *netdef = g_new0(NetplanNetDefinition, 1); reset_netdef(netdef, type, backend); netdef->id = g_strdup(id); if (!npp->parsed_defs) npp->parsed_defs = g_hash_table_new(g_str_hash, g_str_equal); g_hash_table_insert(npp->parsed_defs, netdef->id, netdef); npp->ordered = g_list_append(npp->ordered, netdef); return netdef; } /**************************************************** * Data types and functions for interpreting YAML nodes ****************************************************/ typedef gboolean (*node_handler) (NetplanParser* npp, yaml_node_t* node, const void* data, GError** error); typedef gboolean (*custom_map_handler) (NetplanParser* npp, yaml_node_t* node, const char *prefix, const void* data, GError** error); typedef struct mapping_entry_handler_s { /* mapping key (must be scalar) */ const char* key; /* expected type of the mapped value */ yaml_node_type_t type; union { node_handler generic; custom_map_handler variable; struct { const struct mapping_entry_handler_s* handlers; custom_map_handler custom; } map; }; /* user_data */ const void* data; } mapping_entry_handler; /** * Return the #mapping_entry_handler that matches @key, or NULL if not found. */ STATIC const mapping_entry_handler* get_handler(const mapping_entry_handler* handlers, const char* key) { for (unsigned i = 0; handlers[i].key != NULL; ++i) { if (g_strcmp0(handlers[i].key, key) == 0) return &handlers[i]; } return NULL; } /** * Call handlers for all entries in a YAML mapping. * @doc: The yaml_document_t * @node: The yaml_node_t to process, must be a #YAML_MAPPING_NODE * @handlers: Array of mapping_entry_handler with allowed keys * @error: Gets set on data type errors or unknown keys * * Returns: TRUE on success, FALSE on error (@error gets set then). */ STATIC gboolean process_mapping(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, const mapping_entry_handler* handlers, GList** out_values, GError** error) { yaml_node_pair_t* entry; assert_type(npp, node, YAML_MAPPING_NODE); for (entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { yaml_node_t* key, *value; const mapping_entry_handler* h; gboolean res = TRUE; g_autofree char* full_key = NULL; g_assert(error == NULL || *error == NULL); key = yaml_document_get_node(&npp->doc, entry->key); value = yaml_document_get_node(&npp->doc, entry->value); assert_type(npp, key, YAML_SCALAR_NODE); if (npp->null_fields && key_prefix) { full_key = g_strdup_printf("%s\t%s", key_prefix, scalar(key)); if (g_hash_table_contains(npp->null_fields, full_key)) continue; } h = get_handler(handlers, scalar(key)); if (!h) return yaml_error(npp, key, error, "unknown key '%s'", scalar(key)); assert_type(npp, value, h->type); if (out_values) *out_values = g_list_prepend(*out_values, g_strdup(scalar(key))); if (h->type == YAML_MAPPING_NODE) { if (h->map.custom) res = h->map.custom(npp, value, full_key, h->data, error); else res = process_mapping(npp, value, full_key, h->map.handlers, NULL, error); } else if (h->type == YAML_NO_NODE) { res = h->variable(npp, value, full_key, h->data, error); } else { res = h->generic(npp, value, h->data, error); } if (!res) return FALSE; } return TRUE; } /************************************************************* * Generic helper functions to extract data from scalar nodes. *************************************************************/ /** * Handler for setting a guint field from a scalar node, inside a given struct * @entryptr: pointer to the begining of the to-be-modified data structure * @data: offset into entryptr struct where the guint field to write is located */ STATIC gboolean handle_generic_guint(NetplanParser* npp, yaml_node_t* node, const void* entryptr, const void* data, GError** error) { g_assert(entryptr); guint offset = GPOINTER_TO_UINT(data); guint64 v; gchar* endptr; v = g_ascii_strtoull(scalar(node), &endptr, 10); if (*endptr != '\0' || v > G_MAXUINT) return yaml_error(npp, node, error, "invalid unsigned int value '%s'", scalar(node)); mark_data_as_dirty(npp, entryptr + offset); *((guint*) ((void*) entryptr + offset)) = (guint) v; return TRUE; } /** * Handler for setting a string field from a scalar node, inside a given struct * @entryptr: pointer to the beginning of the to-be-modified data structure * @data: offset into entryptr struct where the const char* field to write is * located */ STATIC gboolean handle_generic_str(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, __unused GError** error) { g_assert(entryptr); guint offset = GPOINTER_TO_UINT(data); char** dest = (char**) ((void*) entryptr + offset); g_free(*dest); *dest = g_strdup(scalar(node)); mark_data_as_dirty(npp, dest); return TRUE; } STATIC gboolean handle_special_macaddress_option(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, GError** error) { g_assert(entryptr); g_assert(node->type == YAML_SCALAR_NODE); if (!_is_macaddress_special_nm_option(scalar(node)) && !_is_macaddress_special_nd_option(scalar(node))) return FALSE; return handle_generic_str(npp, node, entryptr, data, error); } /* * Handler for setting a MAC address field from a scalar node, inside a given struct * @entryptr: pointer to the beginning of the to-be-modified data structure * @data: offset into entryptr struct where the const char* field to write is * located */ STATIC gboolean handle_generic_mac(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, GError** error) { g_assert(entryptr); g_assert(node->type == YAML_SCALAR_NODE); if (!_is_valid_macaddress(scalar(node))) return yaml_error(npp, node, error, "Invalid MAC address '%s', must be XX:XX:XX:XX:XX:XX or XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", scalar(node)); return handle_generic_str(npp, node, entryptr, data, error); } /* * Handler for setting a boolean field from a scalar node, inside a given struct * @entryptr: pointer to the beginning of the to-be-modified data structure * @data: offset into entryptr struct where the boolean field to write is located */ STATIC gboolean handle_generic_bool(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, GError** error) { g_assert(entryptr); guint offset = GPOINTER_TO_UINT(data); gboolean v; gboolean* dest = ((void*) entryptr + offset); if (g_ascii_strcasecmp(scalar(node), "true") == 0 || g_ascii_strcasecmp(scalar(node), "on") == 0 || g_ascii_strcasecmp(scalar(node), "yes") == 0 || g_ascii_strcasecmp(scalar(node), "y") == 0) v = TRUE; else if (g_ascii_strcasecmp(scalar(node), "false") == 0 || g_ascii_strcasecmp(scalar(node), "off") == 0 || g_ascii_strcasecmp(scalar(node), "no") == 0 || g_ascii_strcasecmp(scalar(node), "n") == 0) v = FALSE; else return yaml_error(npp, node, error, "invalid boolean value '%s'", scalar(node)); *dest = v; mark_data_as_dirty(npp, dest); return TRUE; } /* * Handler for setting a HashTable field from a mapping node, inside a given struct * @entryptr: pointer to the beginning of the to-be-modified data structure * @data: offset into entryptr struct where the boolean field to write is located */ STATIC gboolean handle_generic_tristate(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, GError** error) { g_assert(entryptr); NetplanTristate v; guint offset = GPOINTER_TO_UINT(data); NetplanTristate* dest = ((void*) entryptr + offset); if (g_ascii_strcasecmp(scalar(node), "true") == 0 || g_ascii_strcasecmp(scalar(node), "on") == 0 || g_ascii_strcasecmp(scalar(node), "yes") == 0 || g_ascii_strcasecmp(scalar(node), "y") == 0) v = NETPLAN_TRISTATE_TRUE; else if (g_ascii_strcasecmp(scalar(node), "false") == 0 || g_ascii_strcasecmp(scalar(node), "off") == 0 || g_ascii_strcasecmp(scalar(node), "no") == 0 || g_ascii_strcasecmp(scalar(node), "n") == 0) v = NETPLAN_TRISTATE_FALSE; else return yaml_error(npp, node, error, "invalid boolean value '%s'", scalar(node)); *dest = v; mark_data_as_dirty(npp, dest); return TRUE; } /* * Handler for setting a HashTable field from a mapping node, inside a given struct * @entryptr: pointer to the beginning of the to-be-modified data structure * @data: offset into entryptr struct where the boolean field to write is located */ STATIC gboolean handle_generic_map(NetplanParser *npp, yaml_node_t* node, const char* key_prefix, void* entryptr, const void* data, GError** error) { guint offset = GPOINTER_TO_UINT(data); GHashTable** map = (GHashTable**) ((void*) entryptr + offset); if (!*map) *map = g_hash_table_new(g_str_hash, g_str_equal); for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { yaml_node_t* key, *value; key = yaml_document_get_node(&npp->doc, entry->key); value = yaml_document_get_node(&npp->doc, entry->value); assert_type(npp, key, YAML_SCALAR_NODE); assert_type(npp, value, YAML_SCALAR_NODE); if (key_prefix && npp->null_fields) { g_autofree char* full_key = NULL; full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value); if (g_hash_table_contains(npp->null_fields, full_key)) continue; } char* stored_value = NULL; if (g_hash_table_lookup_extended(*map, scalar(key), NULL, (void**)&stored_value)) { /* We can safely skip this if it is the exact key/value match * (probably caused by multi-pass processing) */ if (g_strcmp0(stored_value, scalar(value)) == 0) continue; return yaml_error(npp, node, error, "duplicate map entry '%s'", scalar(key)); } else g_hash_table_insert(*map, g_strdup(scalar(key)), g_strdup(scalar(value))); } mark_data_as_dirty(npp, map); return TRUE; } /* * Handler for setting a DataList field from a mapping node, inside a given struct * @entryptr: pointer to the beginning of the to-be-modified data structure * @data: offset into entryptr struct where the boolean field to write is located */ STATIC gboolean handle_generic_datalist(NetplanParser *npp, yaml_node_t* node, const char* key_prefix, void* entryptr, const void* data, GError** error) { guint offset = GPOINTER_TO_UINT(data); GData** list = (GData**) ((void*) entryptr + offset); if (!*list) g_datalist_init(list); for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { yaml_node_t* key, *value; g_autofree char* full_key = NULL; key = yaml_document_get_node(&npp->doc, entry->key); value = yaml_document_get_node(&npp->doc, entry->value); assert_type(npp, key, YAML_SCALAR_NODE); assert_type(npp, value, YAML_SCALAR_NODE); if (npp->null_fields && key_prefix) { full_key = g_strdup_printf("%s\t%s", key_prefix, scalar(key)); if (g_hash_table_contains(npp->null_fields, full_key)) continue; } g_datalist_id_set_data_full(list, g_quark_from_string(scalar(key)), g_strdup(scalar(value)), g_free); } mark_data_as_dirty(npp, list); return TRUE; } /** * Generic handler for setting a npp->current.netdef string field from a scalar node * @data: offset into NetplanNetDefinition where the const char* field to write is * located */ STATIC gboolean handle_netdef_str(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_str(npp, node, npp->current.netdef, data, error); } /** * Generic handler for setting a npp->current.netdef ID/iface name field from a scalar node * @data: offset into NetplanNetDefinition where the const char* field to write is * located */ STATIC gboolean handle_netdef_id(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (!assert_valid_id(npp, node, error)) return FALSE; return handle_netdef_str(npp, node, data, error); } STATIC gboolean handle_embedded_switch_mode(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (g_strcmp0(scalar(node), "switchdev") != 0 && g_strcmp0(scalar(node), "legacy") != 0) return yaml_error(npp, node, error, "Value of 'embedded-switch-mode' needs to be 'switchdev' or 'legacy'"); return handle_netdef_str(npp, node, data, error); } STATIC gboolean handle_ib_mode(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { if (g_strcmp0(scalar(node), "datagram") == 0) npp->current.netdef->ib_mode = NETPLAN_IB_MODE_DATAGRAM; else if (g_strcmp0(scalar(node), "connected") == 0) npp->current.netdef->ib_mode = NETPLAN_IB_MODE_CONNECTED; else return yaml_error(npp, node, error, "Value of 'infiniband-mode' needs to be 'datagram' or 'connected'"); return TRUE; } /** * Generic handler for setting a npp->current.netdef ID/iface name field referring to an * existing ID from a scalar node. This handler also includes a special case * handler for OVS VLANs, switching the backend implicitly to OVS for such * interfaces * @data: offset into NetplanNetDefinition where the NetplanNetDefinition* field to write is * located */ STATIC gboolean handle_netdef_id_ref(NetplanParser* npp, yaml_node_t* node, const void* data, __unused GError** error) { guint offset = GPOINTER_TO_UINT(data); NetplanNetDefinition* ref = NULL; NetplanNetDefinition** dest = (void*) npp->current.netdef + offset; ref = g_hash_table_lookup(npp->parsed_defs, scalar(node)); if (!ref) { add_missing_node(npp, node); } else { NetplanNetDefinition* netdef = npp->current.netdef; *dest = ref; if (netdef->type == NETPLAN_DEF_TYPE_VLAN && ref->backend == NETPLAN_BACKEND_OVS) { g_debug("%s: VLAN defined for Open vSwitch interface, choosing OVS backend", netdef->id); netdef->backend = NETPLAN_BACKEND_OVS; } } mark_data_as_dirty(npp, dest); return TRUE; } /** * Handler for setting a npp->current.netdef ID/iface name field referring to an * existing ID from a scalar node. * @data: offset into NetplanVxlan where the NetplanNetDefinition* field to * write is located */ STATIC gboolean handle_vxlan_id_ref(NetplanParser* npp, yaml_node_t* node, const void* data, __unused GError** error) { guint offset = GPOINTER_TO_UINT(data); NetplanNetDefinition* ref = NULL; NetplanNetDefinition** dest = (void*) npp->current.vxlan + offset; ref = g_hash_table_lookup(npp->parsed_defs, scalar(node)); if (!ref) add_missing_node(npp, node); else *dest = ref; mark_data_as_dirty(npp, dest); return TRUE; } /** * Generic handler for setting a npp->current.netdef match MAC address field from a scalar node * @data: offset into NetplanNetDefinition where the const char* field to write is * located */ STATIC gboolean handle_netdef_match_mac(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_mac(npp, node, npp->current.netdef, data, error); } /** * Generic handler for setting a npp->current.netdef MAC address field from a scalar node * @data: offset into NetplanNetDefinition where the const char* field to write is * located */ STATIC gboolean handle_netdef_set_mac(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { int res = handle_generic_mac(npp, node, npp->current.netdef, data, NULL); /* If the generic MAC parsing fails, we check to see if the value is one of the special values */ if (!res) { if (!handle_special_macaddress_option(npp, node, npp->current.netdef, data, NULL)) { return yaml_error(npp, node, error, "Invalid MAC address '%s', must be XX:XX:XX:XX:XX:XX, XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX" " or one of 'permanent', 'random', 'stable', 'preserve'.", scalar(node)); } } return TRUE; } /** * Generic handler for setting a npp->current.netdef gboolean field from a scalar node * @data: offset into NetplanNetDefinition where the gboolean field to write is located */ STATIC gboolean handle_netdef_bool(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_bool(npp, node, npp->current.netdef, data, error); } /** * Generic handler for tri-state settings that can bei "UNSET", "TRUE", or "FALSE". * @data: offset into NetplanNetDefinition where the guint field to write is located */ STATIC gboolean handle_netdef_tristate(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_tristate(npp, node, npp->current.netdef, data, error); } /** * Generic handler for setting a npp->current.netdef guint field from a scalar node * @data: offset into NetplanNetDefinition where the guint field to write is located */ STATIC gboolean handle_netdef_guint(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_guint(npp, node, npp->current.netdef, data, error); } STATIC gboolean handle_netdef_ip4(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { guint offset = GPOINTER_TO_UINT(data); char** dest = (char**) ((void*) npp->current.netdef + offset); g_autofree char* addr = NULL; char* prefix_len; /* these addresses can't have /prefix_len */ addr = g_strdup(scalar(node)); prefix_len = strrchr(addr, '/'); /* FIXME: stop excluding this from coverage; refactor address handling instead */ // LCOV_EXCL_START if (prefix_len) return yaml_error(npp, node, error, "invalid address: a single IPv4 address (without /prefixlength) is required"); /* is it an IPv4 address? */ if (!is_ip4_address(addr)) return yaml_error(npp, node, error, "invalid IPv4 address: %s", scalar(node)); // LCOV_EXCL_STOP g_free(*dest); *dest = g_strdup(scalar(node)); mark_data_as_dirty(npp, dest); return TRUE; } STATIC gboolean handle_netdef_ip6(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { guint offset = GPOINTER_TO_UINT(data); char** dest = (char**) ((void*) npp->current.netdef + offset); g_autofree char* addr = NULL; char* prefix_len; /* these addresses can't have /prefix_len */ addr = g_strdup(scalar(node)); prefix_len = strrchr(addr, '/'); /* FIXME: stop excluding this from coverage; refactor address handling instead */ // LCOV_EXCL_START if (prefix_len) return yaml_error(npp, node, error, "invalid address: a single IPv6 address (without /prefixlength) is required"); /* is it an IPv6 address? */ if (!is_ip6_address(addr)) return yaml_error(npp, node, error, "invalid IPv6 address: %s", scalar(node)); // LCOV_EXCL_STOP g_free(*dest); *dest = g_strdup(scalar(node)); mark_data_as_dirty(npp, dest); return TRUE; } STATIC gboolean handle_netdef_addrgen(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { g_assert(npp->current.netdef); if (strcmp(scalar(node), "eui64") == 0) npp->current.netdef->ip6_addr_gen_mode = NETPLAN_ADDRGEN_EUI64; else if (strcmp(scalar(node), "stable-privacy") == 0) npp->current.netdef->ip6_addr_gen_mode = NETPLAN_ADDRGEN_STABLEPRIVACY; else return yaml_error(npp, node, error, "unknown ipv6-address-generation '%s'", scalar(node)); return TRUE; } STATIC gboolean handle_netdef_addrtok(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_assert(npp->current.netdef); gboolean ret = handle_netdef_str(npp, node, data, error); if (!is_ip6_address(npp->current.netdef->ip6_addr_gen_token)) return yaml_error(npp, node, error, "invalid ipv6-address-token '%s'", scalar(node)); return ret; } STATIC gboolean handle_netdef_map(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, const void* data, GError** error) { g_assert(npp->current.netdef); return handle_generic_map(npp, node, key_prefix, npp->current.netdef, data, error); } STATIC gboolean handle_netdef_backend_settings_str(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { npp->current.netdef->has_backend_settings_nm = TRUE; return handle_generic_str(npp, node, npp->current.netdef, data, error); } /* * Check if the passthrough key format is incorrect and remove it from the list. * user_data is expected to contain a pointer to the GData list. */ STATIC void validate_kf_group_key(GQuark key_id, __unused gpointer value, gpointer user_data) { GData** list = user_data; const gchar* key = g_quark_to_string(key_id); gchar** group_key = g_strsplit(key, ".", -1); if (g_strv_length(group_key) < 2) { g_warning("NetworkManager: passthrough key '%s' format is invalid, should be 'group.key'.", key); g_datalist_id_remove_data(list, key_id); } g_strfreev(group_key); } STATIC gboolean handle_netdef_passthrough_datalist(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, const void* data, GError** error) { g_assert(npp->current.netdef); gboolean ret = handle_generic_datalist(npp, node, key_prefix, npp->current.netdef, data, error); GData** list = &npp->current.netdef->backend_settings.passthrough; g_datalist_foreach(list, validate_kf_group_key, list); if (*list == NULL) { g_datalist_clear(list); } npp->current.netdef->has_backend_settings_nm = TRUE; return ret; } STATIC gboolean handle_veth_peer(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { NetplanNetDefinition* netdef = npp->current.netdef; if (!g_strcmp0(netdef->id, scalar(node))) return yaml_error(npp, node, error, "%s: virtual-ethernet peer cannot be itself", netdef->id); NetplanNetDefinition* link = g_hash_table_lookup(npp->parsed_defs, scalar(node)); if (link) { if (link->type != NETPLAN_DEF_TYPE_VETH && link->type != NETPLAN_DEF_TYPE_NM_PLACEHOLDER_) return yaml_error(npp, node, error, "%s: virtual-ethernet peer '%s' is not a virtual-ethernet interface", netdef->id, link->id); if (link->veth_peer_link && link->veth_peer_link != netdef) return yaml_error(npp, node, error, "%s: virtual-ethernet peer '%s' is another virtual-ethernet's (%s) peer already", netdef->id, link->id, link->veth_peer_link->id); netdef->veth_peer_link = link; link->veth_peer_link = netdef; return TRUE; } add_missing_node(npp, node); return TRUE; } /**************************************************** * Grammar and handlers for network config "match" entry ****************************************************/ STATIC gboolean handle_match_driver(NetplanParser* npp, yaml_node_t* node, __unused const char* key_prefix, __unused const void* _, GError** error) { gboolean ret = FALSE; yaml_node_t *elem = NULL; g_autoptr(GString) sequence = NULL; /* We overload the 'driver' setting for matches; such that it can either be a * single scalar specifying a single driver glob/match, or a sequence of many * globs any of which must match. */ if (node->type == YAML_SCALAR_NODE) { if (g_strrstr(scalar(node), " ")) return yaml_error(npp, node, error, "A 'driver' glob cannot contain whitespace"); ret = handle_netdef_str(npp, node, netdef_offset(match.driver), error); } else if (node->type == YAML_SEQUENCE_NODE) { for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) { elem = yaml_document_get_node(&npp->doc, *iter); assert_type(npp, elem, YAML_SCALAR_NODE); if (g_strrstr(scalar(elem), " ")) return yaml_error(npp, node, error, "A 'driver' glob cannot contain whitespace"); if (!sequence) sequence = g_string_new(scalar(elem)); else g_string_append_printf(sequence, "\t%s", scalar(elem)); /* tab separated */ } if (!sequence) return yaml_error(npp, node, error, "invalid sequence for 'driver'"); npp->current.netdef->match.driver = g_strdup(sequence->str); ret = TRUE; } else return yaml_error(npp, node, error, "invalid type for 'driver': must be a scalar or a sequence of scalars"); return ret; } STATIC const mapping_entry_handler match_handlers[] = { {"driver", YAML_NO_NODE, {.variable=handle_match_driver}, NULL}, {"macaddress", YAML_SCALAR_NODE, {.generic=handle_netdef_match_mac}, netdef_offset(match.mac)}, {"name", YAML_SCALAR_NODE, {.generic=handle_netdef_id}, netdef_offset(match.original_name)}, {NULL} }; /**************************************************** * Grammar and handlers for network config "auth" entry ****************************************************/ STATIC gboolean handle_auth_str(NetplanParser* npp, yaml_node_t* node, const void* data, __unused GError** error) { g_assert(npp->current.auth); guint offset = GPOINTER_TO_UINT(data); char** dest = (char**) ((void*) npp->current.auth + offset); g_free(*dest); *dest = g_strdup(scalar(node)); mark_data_as_dirty(npp, dest); return TRUE; } STATIC gboolean handle_auth_key_management(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { NetplanAuthenticationSettings* auth = npp->current.auth; g_assert(auth); if (strcmp(scalar(node), "none") == 0) auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_NONE; else if (strcmp(scalar(node), "psk") == 0) auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK; else if (strcmp(scalar(node), "eap") == 0) auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP; else if (strcmp(scalar(node), "eap-sha256") == 0) { /* WPA-EAP-SHA256 is commonly used with Protected Management Frames * so let's set it as optional */ auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSHA256; auth->pmf_mode = NETPLAN_AUTH_PMF_MODE_OPTIONAL; } else if (strcmp(scalar(node), "eap-suite-b-192") == 0) { /* Settings for WPA3-Enterprise for sensitive enterprise environments. * Protected Management Frames (ieee80211w) is mandatory. */ auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAPSUITE_B_192; auth->pmf_mode = NETPLAN_AUTH_PMF_MODE_REQUIRED; } else if (strcmp(scalar(node), "sae") == 0) { /* SAE is used by WPA3 and Protected Management Frames * (ieee80211w) is mandatory. */ auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_SAE; auth->pmf_mode = NETPLAN_AUTH_PMF_MODE_REQUIRED; } else if (strcmp(scalar(node), "802.1x") == 0) auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_8021X; else return yaml_error(npp, node, error, "unknown key management type '%s'", scalar(node)); return TRUE; } STATIC gboolean handle_auth_method(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { NetplanAuthenticationSettings* auth = npp->current.auth; g_assert(auth); if (strcmp(scalar(node), "tls") == 0) auth->eap_method = NETPLAN_AUTH_EAP_TLS; else if (strcmp(scalar(node), "peap") == 0) auth->eap_method = NETPLAN_AUTH_EAP_PEAP; else if (strcmp(scalar(node), "ttls") == 0) auth->eap_method = NETPLAN_AUTH_EAP_TTLS; else if (strcmp(scalar(node), "leap") == 0) auth->eap_method = NETPLAN_AUTH_EAP_LEAP; else if (strcmp(scalar(node), "pwd") == 0) auth->eap_method = NETPLAN_AUTH_EAP_PWD; else return yaml_error(npp, node, error, "unknown EAP method '%s'", scalar(node)); return TRUE; } STATIC const mapping_entry_handler auth_handlers[] = { {"key-management", YAML_SCALAR_NODE, {.generic=handle_auth_key_management}, NULL}, {"method", YAML_SCALAR_NODE, {.generic=handle_auth_method}, NULL}, {"identity", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(identity)}, {"anonymous-identity", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(anonymous_identity)}, {"password", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(password)}, {"ca-certificate", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(ca_certificate)}, {"client-certificate", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(client_certificate)}, {"client-key", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(client_key)}, {"client-key-password", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(client_key_password)}, {"phase2-auth", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(phase2_auth)}, {NULL} }; /**************************************************** * Grammar and handlers for network device definition ****************************************************/ NetplanBackend get_default_backend_for_type(NetplanBackend global_backend, __unused NetplanDefType type) { if (global_backend != NETPLAN_BACKEND_NONE) return global_backend; /* networkd can handle all device types at the moment, so nothing * type-specific */ return NETPLAN_BACKEND_NETWORKD; } STATIC gboolean handle_ap_backend_settings_str(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { npp->current.netdef->has_backend_settings_nm = TRUE; return handle_generic_str(npp, node, npp->current.access_point, data, error); } STATIC gboolean handle_access_point_datalist(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, const void* data, GError** error) { g_assert(npp->current.access_point); gboolean ret = handle_generic_datalist(npp, node, key_prefix, npp->current.access_point, data, error); GData** list = &npp->current.access_point->backend_settings.passthrough; g_datalist_foreach(list, validate_kf_group_key, list); if (*list == NULL) { g_datalist_clear(list); } npp->current.netdef->has_backend_settings_nm = TRUE; return ret; } STATIC gboolean handle_access_point_guint(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_guint(npp, node, npp->current.access_point, data, error); } STATIC gboolean handle_access_point_mac(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_mac(npp, node, npp->current.access_point, data, error); } STATIC gboolean handle_access_point_bool(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_bool(npp, node, npp->current.access_point, data, error); } STATIC gboolean handle_access_point_password(NetplanParser* npp, yaml_node_t* node, __unused const void* _, __unused GError** error) { NetplanWifiAccessPoint *access_point = npp->current.access_point; g_assert(access_point); /* shortcut for WPA-PSK */ access_point->has_auth = TRUE; if (access_point->auth.key_management == NETPLAN_AUTH_KEY_MANAGEMENT_NONE) access_point->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK; access_point->auth.pmf_mode = NETPLAN_AUTH_PMF_MODE_OPTIONAL; g_free(access_point->auth.psk); access_point->auth.psk = g_strdup(scalar(node)); return TRUE; } STATIC gboolean handle_access_point_auth(NetplanParser* npp, yaml_node_t* node, __unused const char* key_prefix, __unused const void* _, GError** error) { NetplanWifiAccessPoint *access_point = npp->current.access_point; gboolean ret; g_assert(access_point); access_point->has_auth = TRUE; npp->current.auth = &access_point->auth; ret = process_mapping(npp, node, NULL, auth_handlers, NULL, error); npp->current.auth = NULL; return ret; } STATIC gboolean handle_access_point_mode(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { NetplanWifiAccessPoint *access_point = npp->current.access_point; g_assert(access_point); if (strcmp(scalar(node), "infrastructure") == 0) access_point->mode = NETPLAN_WIFI_MODE_INFRASTRUCTURE; else if (strcmp(scalar(node), "adhoc") == 0) access_point->mode = NETPLAN_WIFI_MODE_ADHOC; else if (strcmp(scalar(node), "ap") == 0) access_point->mode = NETPLAN_WIFI_MODE_AP; else return yaml_error(npp, node, error, "unknown wifi mode '%s'", scalar(node)); return TRUE; } STATIC gboolean handle_access_point_band(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { NetplanWifiAccessPoint *access_point = npp->current.access_point; g_assert(access_point); if (strcmp(scalar(node), "5GHz") == 0 || strcmp(scalar(node), "5G") == 0) access_point->band = NETPLAN_WIFI_BAND_5; else if (strcmp(scalar(node), "2.4GHz") == 0 || strcmp(scalar(node), "2.4G") == 0) access_point->band = NETPLAN_WIFI_BAND_24; else return yaml_error(npp, node, error, "unknown wifi band '%s'", scalar(node)); return TRUE; } STATIC gboolean handle_tunnel_key_flags(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); gboolean found = FALSE; assert_type(npp, entry, YAML_SCALAR_NODE); for (int i = 1; i < NETPLAN_KEY_FLAG_MAX_; i <<= 1) { if (!g_ascii_strcasecmp(scalar(entry), netplan_key_flags_name(i))) { npp->current.netdef->tunnel_private_key_flags |= i; found = TRUE; } } if (!found) return yaml_error(npp, node, error, "Key flag '%s' is not supported. Valid values are \"agent-owned\", \"not-saved\" and \"not-required\"", scalar(entry)); } return TRUE; } /* Keep in sync with ap_nm_backend_settings_handlers */ static const mapping_entry_handler nm_backend_settings_handlers[] = { {"name", YAML_SCALAR_NODE, {.generic=handle_netdef_backend_settings_str}, netdef_offset(backend_settings.name)}, {"uuid", YAML_SCALAR_NODE, {.generic=handle_netdef_backend_settings_str}, netdef_offset(backend_settings.uuid)}, {"stable-id", YAML_SCALAR_NODE, {.generic=handle_netdef_backend_settings_str}, netdef_offset(backend_settings.stable_id)}, {"device", YAML_SCALAR_NODE, {.generic=handle_netdef_backend_settings_str}, netdef_offset(backend_settings.device)}, /* Fallback mode, to support all NM settings of the NetworkManager netplan backend */ {"passthrough", YAML_MAPPING_NODE, {.map={.custom=handle_netdef_passthrough_datalist}}, netdef_offset(backend_settings.passthrough)}, {NULL} }; /* Keep in sync with nm_backend_settings_handlers */ static const mapping_entry_handler ap_nm_backend_settings_handlers[] = { {"name", YAML_SCALAR_NODE, {.generic=handle_ap_backend_settings_str}, access_point_offset(backend_settings.name)}, {"uuid", YAML_SCALAR_NODE, {.generic=handle_ap_backend_settings_str}, access_point_offset(backend_settings.uuid)}, {"stable-id", YAML_SCALAR_NODE, {.generic=handle_ap_backend_settings_str}, access_point_offset(backend_settings.stable_id)}, {"device", YAML_SCALAR_NODE, {.generic=handle_ap_backend_settings_str}, access_point_offset(backend_settings.device)}, /* Fallback mode, to support all NM settings of the NetworkManager netplan backend */ {"passthrough", YAML_MAPPING_NODE, {.map={.custom=handle_access_point_datalist}}, access_point_offset(backend_settings.passthrough)}, {NULL} }; static const mapping_entry_handler wifi_access_point_handlers[] = { {"band", YAML_SCALAR_NODE, {.generic=handle_access_point_band}, NULL}, {"bssid", YAML_SCALAR_NODE, {.generic=handle_access_point_mac}, access_point_offset(bssid)}, {"hidden", YAML_SCALAR_NODE, {.generic=handle_access_point_bool}, access_point_offset(hidden)}, {"channel", YAML_SCALAR_NODE, {.generic=handle_access_point_guint}, access_point_offset(channel)}, {"mode", YAML_SCALAR_NODE, {.generic=handle_access_point_mode}, NULL}, {"password", YAML_SCALAR_NODE, {.generic=handle_access_point_password}, NULL}, {"auth", YAML_MAPPING_NODE, {.map={.custom=handle_access_point_auth}}, NULL}, {"networkmanager", YAML_MAPPING_NODE, {.map={.handlers=ap_nm_backend_settings_handlers}}, NULL}, {NULL} }; /** * Parse scalar node's string into a netdef_backend. */ STATIC gboolean parse_renderer(NetplanParser* npp, yaml_node_t* node, NetplanBackend* backend, GError** error) { if (strcmp(scalar(node), "networkd") == 0) *backend = NETPLAN_BACKEND_NETWORKD; else if (strcmp(scalar(node), "NetworkManager") == 0) *backend = NETPLAN_BACKEND_NM; else return yaml_error(npp, node, error, "unknown renderer '%s'", scalar(node)); mark_data_as_dirty(npp, backend); return TRUE; } STATIC gboolean handle_netdef_renderer(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { if (npp->current.netdef->type == NETPLAN_DEF_TYPE_VLAN) { if (strcmp(scalar(node), "sriov") == 0) { npp->current.netdef->sriov_vlan_filter = TRUE; return TRUE; } } return parse_renderer(npp, node, &npp->current.netdef->backend, error); } STATIC gboolean handle_accept_ra(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { gboolean ret = handle_generic_bool(npp, node, npp->current.netdef, data, error); if (npp->current.netdef->accept_ra) npp->current.netdef->accept_ra = NETPLAN_RA_MODE_ENABLED; else npp->current.netdef->accept_ra = NETPLAN_RA_MODE_DISABLED; return ret; } STATIC gboolean handle_activation_mode(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (g_strcmp0(scalar(node), "manual") && g_strcmp0(scalar(node), "off")) return yaml_error(npp, node, error, "Value of 'activation-mode' needs to be 'manual' or 'off'"); return handle_netdef_str(npp, node, data, error); } STATIC gboolean handle_match(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, __unused const void* _, GError** error) { npp->current.netdef->has_match = TRUE; return process_mapping(npp, node, key_prefix, match_handlers, NULL, error); } struct NetplanWifiWowlanType NETPLAN_WIFI_WOWLAN_TYPES[] = { {"default", NETPLAN_WIFI_WOWLAN_DEFAULT}, {"any", NETPLAN_WIFI_WOWLAN_ANY}, {"disconnect", NETPLAN_WIFI_WOWLAN_DISCONNECT}, {"magic_pkt", NETPLAN_WIFI_WOWLAN_MAGIC}, {"gtk_rekey_failure", NETPLAN_WIFI_WOWLAN_GTK_REKEY_FAILURE}, {"eap_identity_req", NETPLAN_WIFI_WOWLAN_EAP_IDENTITY_REQ}, {"four_way_handshake", NETPLAN_WIFI_WOWLAN_4WAY_HANDSHAKE}, {"rfkill_release", NETPLAN_WIFI_WOWLAN_RFKILL_RELEASE}, {"tcp", NETPLAN_WIFI_WOWLAN_TCP}, {NULL}, }; STATIC gboolean handle_wowlan(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); assert_type(npp, entry, YAML_SCALAR_NODE); int found = FALSE; for (unsigned i = 0; NETPLAN_WIFI_WOWLAN_TYPES[i].name != NULL; ++i) { if (g_ascii_strcasecmp(scalar(entry), NETPLAN_WIFI_WOWLAN_TYPES[i].name) == 0) { npp->current.netdef->wowlan |= NETPLAN_WIFI_WOWLAN_TYPES[i].flag; found = TRUE; break; } } if (!found) return yaml_error(npp, node, error, "invalid value for wakeonwlan: '%s'", scalar(entry)); } if (npp->current.netdef->wowlan > NETPLAN_WIFI_WOWLAN_DEFAULT && npp->current.netdef->wowlan & NETPLAN_WIFI_WOWLAN_TYPES[0].flag) return yaml_error(npp, node, error, "'default' is an exclusive flag for wakeonwlan"); return TRUE; } STATIC gboolean handle_auth(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, __unused const void* _, GError** error) { gboolean ret; npp->current.netdef->has_auth = TRUE; npp->current.auth = &npp->current.netdef->auth; ret = process_mapping(npp, node, key_prefix, auth_handlers, NULL, error); mark_data_as_dirty(npp, &npp->current.netdef->auth); npp->current.auth = NULL; return ret; } STATIC gboolean handle_address_option_lifetime(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (g_ascii_strcasecmp(scalar(node), "0") != 0 && g_ascii_strcasecmp(scalar(node), "forever") != 0) { return yaml_error(npp, node, error, "invalid lifetime value '%s'", scalar(node)); } return handle_generic_str(npp, node, npp->current.addr_options, data, error); } STATIC gboolean handle_address_option_label(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_str(npp, node, npp->current.addr_options, data, error); } const mapping_entry_handler address_option_handlers[] = { {"lifetime", YAML_SCALAR_NODE, {.generic=handle_address_option_lifetime}, addr_option_offset(lifetime)}, {"label", YAML_SCALAR_NODE, {.generic=handle_address_option_label}, addr_option_offset(label)}, {NULL} }; /* * Handler for setting an array of IP addresses from a sequence node, inside a given struct * @entryptr: pointer to the beginning of the do-be-modified data structure * @data: offset into entryptr struct where the array to write is located */ STATIC gboolean handle_generic_addresses(NetplanParser* npp, yaml_node_t* node, gboolean check_zero_prefix, GArray** ip4, GArray** ip6, GError** error) { g_assert(ip4); g_assert(ip6); for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { g_autofree char* addr = NULL; char* prefix_len; guint64 prefix_len_num; yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); yaml_node_t *key = NULL; yaml_node_t *value = NULL; if (entry->type != YAML_SCALAR_NODE && entry->type != YAML_MAPPING_NODE) { return yaml_error(npp, entry, error, "expected either scalar or mapping (check indentation)"); } if (entry->type == YAML_MAPPING_NODE) { key = yaml_document_get_node(&npp->doc, entry->data.mapping.pairs.start->key); value = yaml_document_get_node(&npp->doc, entry->data.mapping.pairs.start->value); entry = key; } assert_type(npp, entry, YAML_SCALAR_NODE); /* split off /prefix_len */ addr = g_strdup(scalar(entry)); prefix_len = strrchr(addr, '/'); if (!prefix_len) return yaml_error(npp, node, error, "address '%s' is missing /prefixlength", scalar(entry)); *prefix_len = '\0'; prefix_len++; /* skip former '/' into first char of prefix */ prefix_len_num = g_ascii_strtoull(prefix_len, NULL, 10); if (value) { if (!is_ip4_address(addr) && !is_ip6_address(addr)) return yaml_error(npp, node, error, "malformed address '%s', must be X.X.X.X/NN or X:X:X:X:X:X:X:X/NN", scalar(entry)); if (!npp->current.netdef->address_options) npp->current.netdef->address_options = g_array_new(FALSE, FALSE, sizeof(NetplanAddressOptions*)); for (unsigned i = 0; i < npp->current.netdef->address_options->len; ++i) { NetplanAddressOptions* opts = g_array_index(npp->current.netdef->address_options, NetplanAddressOptions*, i); /* check for multi-pass parsing, return early if options for this address already exist */ if (!g_strcmp0(scalar(key), opts->address)) return TRUE; } npp->current.addr_options = g_new0(NetplanAddressOptions, 1); npp->current.addr_options->address = g_strdup(scalar(key)); if (!process_mapping(npp, value, NULL, address_option_handlers, NULL, error)) return FALSE; g_array_append_val(npp->current.netdef->address_options, npp->current.addr_options); mark_data_as_dirty(npp, &npp->current.netdef->address_options); npp->current.addr_options = NULL; continue; } /* is it an IPv4 address? */ if (is_ip4_address(addr)) { if ((check_zero_prefix && prefix_len_num == 0) || prefix_len_num > 32) return yaml_error(npp, node, error, "invalid prefix length in address '%s'", scalar(entry)); if (!*ip4) *ip4 = g_array_new(FALSE, FALSE, sizeof(char*)); /* Do not append the same IP (on multiple passes), if it is already contained */ for (unsigned i = 0; i < (*ip4)->len; ++i) if (!g_strcmp0(scalar(entry), g_array_index(*ip4, char*, i))) goto skip_ip4; char* s = g_strdup(scalar(entry)); g_array_append_val(*ip4, s); mark_data_as_dirty(npp, ip4); skip_ip4: continue; } /* is it an IPv6 address? */ if (is_ip6_address(addr)) { if ((check_zero_prefix && prefix_len_num == 0) || prefix_len_num > 128) return yaml_error(npp, node, error, "invalid prefix length in address '%s'", scalar(entry)); if (!*ip6) *ip6 = g_array_new(FALSE, FALSE, sizeof(char*)); /* Do not append the same IP (on multiple passes), if it is already contained */ for (unsigned i = 0; i < (*ip6)->len; ++i) if (!g_strcmp0(scalar(entry), g_array_index(*ip6, char*, i))) goto skip_ip6; char* s = g_strdup(scalar(entry)); g_array_append_val(*ip6, s); mark_data_as_dirty(npp, ip6); skip_ip6: continue; } return yaml_error(npp, node, error, "malformed address '%s', must be X.X.X.X/NN or X:X:X:X:X:X:X:X/NN", scalar(entry)); } return TRUE; } STATIC gboolean handle_addresses(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { return handle_generic_addresses(npp, node, TRUE, &(npp->current.netdef->ip4_addresses), &(npp->current.netdef->ip6_addresses), error); } STATIC gboolean handle_gateway4(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { if (!is_ip4_address(scalar(node))) return yaml_error(npp, node, error, "invalid IPv4 address '%s'", scalar(node)); set_str_if_null(npp->current.netdef->gateway4, scalar(node)); mark_data_as_dirty(npp, &npp->current.netdef->gateway4); g_warning("`gateway4` has been deprecated, use default routes instead.\n" "See the 'Default routes' section of the documentation for more details."); return TRUE; } STATIC gboolean handle_gateway6(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { if (!is_ip6_address(scalar(node))) return yaml_error(npp, node, error, "invalid IPv6 address '%s'", scalar(node)); set_str_if_null(npp->current.netdef->gateway6, scalar(node)); mark_data_as_dirty(npp, &npp->current.netdef->gateway6); g_warning("`gateway6` has been deprecated, use default routes instead.\n" "See the 'Default routes' section of the documentation for more details."); return TRUE; } STATIC gboolean handle_wifi_access_points(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, __unused const void* data, GError** error) { GHashTable* access_points = g_hash_table_new(g_str_hash, g_str_equal); for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { NetplanWifiAccessPoint *access_point = NULL; g_autofree char* full_key = NULL; yaml_node_t* key, *value; const gchar* ssid; key = yaml_document_get_node(&npp->doc, entry->key); assert_type(npp, key, YAML_SCALAR_NODE); value = yaml_document_get_node(&npp->doc, entry->value); assert_type(npp, value, YAML_MAPPING_NODE); if (key_prefix && npp->null_fields) { full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value); if (g_hash_table_contains(npp->null_fields, full_key)) continue; } ssid = scalar(key); /* * Delete the access-point if it already exists in the netdef and let the new * one be added. It has the side effect of reprocessing APs if the parser requires a * second pass. * * TODO: implement support for merging AP settings if they were previously defined */ if (npp->current.netdef->access_points && g_hash_table_contains(npp->current.netdef->access_points, ssid)) { NetplanWifiAccessPoint *ap = g_hash_table_lookup(npp->current.netdef->access_points, ssid); g_hash_table_remove(npp->current.netdef->access_points, ssid); free_access_point(NULL, ap, NULL); } /* Check if the SSID was already defined in the same netdef in this YAML file we are parsing */ if (g_hash_table_contains(access_points, ssid)) { g_hash_table_foreach(access_points, free_access_point, NULL); g_hash_table_destroy(access_points); return yaml_error(npp, key, error, "%s: Duplicate access point SSID '%s'", npp->current.netdef->id, ssid); } g_assert(access_point == NULL); access_point = g_new0(NetplanWifiAccessPoint, 1); access_point->ssid = g_strdup(ssid); g_debug("%s: adding wifi AP '%s'", npp->current.netdef->id, access_point->ssid); npp->current.access_point = access_point; if (!process_mapping(npp, value, full_key, wifi_access_point_handlers, NULL, error)) { access_point_clear(&npp->current.access_point, npp->current.backend); g_hash_table_foreach(access_points, free_access_point, NULL); g_hash_table_destroy(access_points); return FALSE; } g_hash_table_insert(access_points, access_point->ssid, access_point); npp->current.access_point = NULL; } if (g_hash_table_size(access_points) > 0) { if (!npp->current.netdef->access_points) npp->current.netdef->access_points = g_hash_table_new(g_str_hash, g_str_equal); g_hash_table_foreach_steal(access_points, insert_kv_into_hash, npp->current.netdef->access_points); mark_data_as_dirty(npp, &npp->current.netdef->access_points); } g_hash_table_destroy(access_points); return TRUE; } /** * Handler for bridge "interfaces:" list. We don't store that list in npp->current.netdef, * but set npp->current.netdef's ID in all listed interfaces' "bond" or "bridge" field. * @data: ignored */ STATIC gboolean handle_bridge_interfaces(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { /* all entries must refer to already defined IDs */ for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); NetplanNetDefinition *component; assert_type(npp, entry, YAML_SCALAR_NODE); component = g_hash_table_lookup(npp->parsed_defs, scalar(entry)); if (!component) { add_missing_node(npp, entry); } else { if (component->bridge && g_strcmp0(component->bridge, npp->current.netdef->id) != 0) return yaml_error(npp, node, error, "%s: interface '%s' is already assigned to bridge %s", npp->current.netdef->id, scalar(entry), component->bridge); if (component->bond) return yaml_error(npp, node, error, "%s: interface '%s' is already assigned to bond %s", npp->current.netdef->id, scalar(entry), component->bond); set_str_if_null(component->bridge, npp->current.netdef->id); component->bridge_link = npp->current.netdef; if (component->backend == NETPLAN_BACKEND_OVS) { g_debug("%s: Bridge contains Open vSwitch interface, choosing OVS backend", npp->current.netdef->id); npp->current.netdef->backend = NETPLAN_BACKEND_OVS; } } } return TRUE; } /** * Handler for bond "mode" types. * @data: offset into NetplanNetDefinition where the const char* field to write is * located */ STATIC gboolean handle_bond_mode(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (!(strcmp(scalar(node), "balance-rr") == 0 || strcmp(scalar(node), "active-backup") == 0 || strcmp(scalar(node), "balance-xor") == 0 || strcmp(scalar(node), "broadcast") == 0 || strcmp(scalar(node), "802.3ad") == 0 || strcmp(scalar(node), "balance-tlb") == 0 || strcmp(scalar(node), "balance-alb") == 0 || strcmp(scalar(node), "balance-tcp") == 0 || // only supported for OVS strcmp(scalar(node), "balance-slb") == 0)) // only supported for OVS return yaml_error(npp, node, error, "unknown bond mode '%s'", scalar(node)); /* Implicitly set NETPLAN_BACKEND_OVS if ovs-only mode selected */ if (!strcmp(scalar(node), "balance-tcp") || !strcmp(scalar(node), "balance-slb")) { g_debug("%s: mode '%s' only supported with Open vSwitch, choosing this backend", npp->current.netdef->id, scalar(node)); npp->current.netdef->backend = NETPLAN_BACKEND_OVS; } return handle_netdef_str(npp, node, data, error); } /** * Handler for bond "interfaces:" list. * @data: ignored */ STATIC gboolean handle_bond_interfaces(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { /* all entries must refer to already defined IDs */ for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); NetplanNetDefinition *component; assert_type(npp, entry, YAML_SCALAR_NODE); component = g_hash_table_lookup(npp->parsed_defs, scalar(entry)); if (!component) { add_missing_node(npp, entry); } else { if (component->bridge) return yaml_error(npp, node, error, "%s: interface '%s' is already assigned to bridge %s", npp->current.netdef->id, scalar(entry), component->bridge); if (component->bond && g_strcmp0(component->bond, npp->current.netdef->id) != 0) return yaml_error(npp, node, error, "%s: interface '%s' is already assigned to bond %s", npp->current.netdef->id, scalar(entry), component->bond); if (!component->bond) { component->bond = g_strdup(npp->current.netdef->id); component->bond_link = npp->current.netdef; } if (component->backend == NETPLAN_BACKEND_OVS) { g_debug("%s: Bond contains Open vSwitch interface, choosing OVS backend", npp->current.netdef->id); npp->current.netdef->backend = NETPLAN_BACKEND_OVS; } } } return TRUE; } STATIC gboolean handle_nameservers_search(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); assert_type(npp, entry, YAML_SCALAR_NODE); if (!npp->current.netdef->search_domains) npp->current.netdef->search_domains = g_array_new(FALSE, FALSE, sizeof(char*)); if (!is_string_in_array(npp->current.netdef->search_domains, scalar(entry))) { char* s = g_strdup(scalar(entry)); g_array_append_val(npp->current.netdef->search_domains, s); } else { g_debug("%s: Search domain '%s' has already been added", npp->current.netdef->id, scalar(entry)); } } mark_data_as_dirty(npp, &npp->current.netdef->search_domains); return TRUE; } STATIC gboolean handle_nameservers_addresses(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { GArray **nameservers = NULL; yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); assert_type(npp, entry, YAML_SCALAR_NODE); /* is it an IPv4 or IPv6 address? */ if (is_ip4_address(scalar(entry))) nameservers = &npp->current.netdef->ip4_nameservers; else if (is_ip6_address(scalar(entry))) nameservers = &npp->current.netdef->ip6_nameservers; else return yaml_error(npp, node, error, "malformed address '%s', must be X.X.X.X or X:X:X:X:X:X:X:X", scalar(entry)); if (!(*nameservers)) *nameservers = g_array_new(FALSE, FALSE, sizeof(char*)); if (!is_string_in_array(*nameservers, scalar(entry))) { char* s = g_strdup(scalar(entry)); g_array_append_val(*nameservers, s); } else { g_debug("%s: Nameserver '%s' has already been added", npp->current.netdef->id, scalar(entry)); } } mark_data_as_dirty(npp, &npp->current.netdef->ip4_nameservers); mark_data_as_dirty(npp, &npp->current.netdef->ip6_nameservers); return TRUE; } STATIC gboolean handle_link_local(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { gboolean ipv4 = FALSE; gboolean ipv6 = FALSE; for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); assert_type(npp, entry, YAML_SCALAR_NODE); if (g_ascii_strcasecmp(scalar(entry), "ipv4") == 0) { ipv4 = TRUE; mark_data_as_dirty(npp, &npp->current.netdef->linklocal.ipv4); } else if (g_ascii_strcasecmp(scalar(entry), "ipv6") == 0) { ipv6 = TRUE; mark_data_as_dirty(npp, &npp->current.netdef->linklocal.ipv6); } else return yaml_error(npp, node, error, "invalid value for link-local: '%s'", scalar(entry)); } npp->current.netdef->linklocal.ipv4 = ipv4; npp->current.netdef->linklocal.ipv6 = ipv6; return TRUE; } struct NetplanOptionalAddressType NETPLAN_OPTIONAL_ADDRESS_TYPES[] = { {"ipv4-ll", NETPLAN_OPTIONAL_IPV4_LL}, {"ipv6-ra", NETPLAN_OPTIONAL_IPV6_RA}, {"dhcp4", NETPLAN_OPTIONAL_DHCP4}, {"dhcp6", NETPLAN_OPTIONAL_DHCP6}, {"static", NETPLAN_OPTIONAL_STATIC}, {NULL}, }; STATIC gboolean handle_optional_addresses(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); assert_type(npp, entry, YAML_SCALAR_NODE); int found = FALSE; for (unsigned i = 0; NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name != NULL; ++i) { if (g_ascii_strcasecmp(scalar(entry), NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name) == 0) { npp->current.netdef->optional_addresses |= NETPLAN_OPTIONAL_ADDRESS_TYPES[i].flag; found = TRUE; break; } } if (!found) { return yaml_error(npp, node, error, "invalid value for optional-addresses: '%s'", scalar(entry)); } } return TRUE; } /* TODO: unify optional_addresses/wowlan_types, using flags */ STATIC gboolean handle_vxlan_flags(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_assert(npp->current.vxlan); assert_type(npp, node, YAML_SEQUENCE_NODE); yaml_node_t* key_node = node-1; // The YAML key of given sequence `node` guint offset = GPOINTER_TO_UINT(data); const char* const* flags = NULL; guint flags_size = 0; NetplanFlags* out_ptr = NULL; switch (offset) { case offsetof(NetplanVxlan, notifications): out_ptr = &npp->current.vxlan->notifications; flags = netplan_vxlan_notification_to_str; flags_size = sizeof(netplan_vxlan_notification_to_str); break; case offsetof(NetplanVxlan, checksums): out_ptr = &npp->current.vxlan->checksums; flags = netplan_vxlan_checksum_to_str; flags_size = sizeof(netplan_vxlan_checksum_to_str); break; case offsetof(NetplanVxlan, extensions): out_ptr = &npp->current.vxlan->extensions; flags = netplan_vxlan_extension_to_str; flags_size = sizeof(netplan_vxlan_extension_to_str); break; default: g_assert_not_reached(); // LCOV_EXCL_LINE } g_assert(flags); g_assert(out_ptr); for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); assert_type(npp, entry, YAML_SCALAR_NODE); int found = FALSE; /* Loop through the flags to find a matching string. * Once found, shift a bit to position INDEX-1 and use bitwise OR to * apply it to the corresponding flags field (i.e. *out_ptr) */ // The minimum flag is always 0x1 (i.e. 0b0001), so start the loop at 1. for (unsigned j = 1; j < flags_size/sizeof(char*); ++j) { if (g_ascii_strcasecmp(scalar(entry), flags[j]) == 0) { *out_ptr |= 1<<(j-1); mark_data_as_dirty(npp, out_ptr); found = TRUE; break; } } if (!found) { return yaml_error(npp, node, error, "invalid value for %s: '%s'", scalar(key_node), scalar(entry)); } } return TRUE; } STATIC gboolean handle_vxlan_guint(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_assert(npp->current.vxlan); return handle_generic_guint(npp, node, npp->current.vxlan, data, error); } STATIC gboolean handle_vxlan_tristate(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_assert(npp->current.vxlan); return handle_generic_tristate(npp, node, npp->current.vxlan, data, error); } STATIC int get_ip_family(const char* address) { g_autofree char *ip_str; char *prefix_len; ip_str = g_strdup(address); prefix_len = strrchr(ip_str, '/'); if (prefix_len) *prefix_len = '\0'; if (is_ip4_address(ip_str)) return AF_INET; if (is_ip6_address(ip_str)) return AF_INET6; return -1; } STATIC gboolean check_and_set_family(gint family, gint* dest) { if (*dest != -1 && *dest != family) return FALSE; *dest = family; return TRUE; } /* TODO: (cyphermox) Refactor the functions below. There's a lot of room for reuse. */ STATIC gboolean handle_routes_bool(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_assert(npp->current.route); return handle_generic_bool(npp, node, npp->current.route, data, error); } STATIC gboolean handle_routes_scope(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { NetplanIPRoute* route = npp->current.route; if (route->scope) g_free(route->scope); route->scope = g_strdup(scalar(node)); if (g_ascii_strcasecmp(route->scope, "global") == 0 || g_ascii_strcasecmp(route->scope, "link") == 0 || g_ascii_strcasecmp(route->scope, "host") == 0) return TRUE; return yaml_error(npp, node, error, "invalid route scope '%s'", route->scope); } STATIC gboolean handle_routes_type(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { NetplanIPRoute* route = npp->current.route; if (route->type) g_free(route->type); route->type = g_strdup(scalar(node)); /* local, broadcast, anycast, multicast, nat and xresolve are supported * since systemd-networkd v243 */ /* keep "unicast" default at position 1 */ if ( g_ascii_strcasecmp(route->type, "unicast") == 0 || g_ascii_strcasecmp(route->type, "anycast") == 0 || g_ascii_strcasecmp(route->type, "blackhole") == 0 || g_ascii_strcasecmp(route->type, "broadcast") == 0 || g_ascii_strcasecmp(route->type, "local") == 0 || g_ascii_strcasecmp(route->type, "multicast") == 0 || g_ascii_strcasecmp(route->type, "nat") == 0 || g_ascii_strcasecmp(route->type, "prohibit") == 0 || g_ascii_strcasecmp(route->type, "throw") == 0 || g_ascii_strcasecmp(route->type, "unreachable") == 0 || g_ascii_strcasecmp(route->type, "xresolve") == 0) return TRUE; return yaml_error(npp, node, error, "invalid route type '%s'", route->type); } STATIC gboolean handle_routes_ip(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { NetplanIPRoute* route = npp->current.route; guint offset = GPOINTER_TO_UINT(data); int family = get_ip_family(scalar(node)); char** dest = (char**) ((void*) route + offset); if (family < 0) return yaml_error(npp, node, error, "invalid IP family '%d'", family); if (!check_and_set_family(family, &route->family)) return yaml_error(npp, node, error, "IP family mismatch in route to %s", scalar(node)); g_free(*dest); *dest = g_strdup(scalar(node)); mark_data_as_dirty(npp, dest); return TRUE; } STATIC gboolean handle_routes_destination(NetplanParser *npp, yaml_node_t *node, __unused const void *data, GError **error) { const char *addr = scalar(node); if (g_strcmp0(addr, "default") != 0) /* netplan-feature: default-routes */ return handle_routes_ip(npp, node, route_offset(to), error); set_str_if_null(npp->current.route->to, addr); return TRUE; } STATIC gboolean handle_ip_rule_ip(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { NetplanIPRule* ip_rule = npp->current.ip_rule; guint offset = GPOINTER_TO_UINT(data); int family = get_ip_family(scalar(node)); char** dest = (char**) ((void*) ip_rule + offset); if (family < 0) return yaml_error(npp, node, error, "invalid IP family '%d'", family); if (!check_and_set_family(family, &ip_rule->family)) return yaml_error(npp, node, error, "IP family mismatch in route to %s", scalar(node)); g_free(*dest); *dest = g_strdup(scalar(node)); mark_data_as_dirty(npp, dest); return TRUE; } STATIC gboolean handle_ip_rule_guint(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_assert(npp->current.ip_rule); return handle_generic_guint(npp, node, npp->current.ip_rule, data, error); } STATIC gboolean handle_routes_guint(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_assert(npp->current.route); return handle_generic_guint(npp, node, npp->current.route, data, error); } STATIC gboolean handle_ip_rule_tos(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { NetplanIPRule* ip_rule = npp->current.ip_rule; gboolean ret = handle_generic_guint(npp, node, ip_rule, data, error); if (ip_rule->tos > 255) return yaml_error(npp, node, error, "invalid ToS (must be between 0 and 255): %s", scalar(node)); return ret; } /**************************************************** * Grammar and handlers for network config "bridge_params" entry ****************************************************/ STATIC gboolean handle_bridge_path_cost(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, const void* data, GError** error) { for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { yaml_node_t* key, *value; guint v; gchar* endptr; NetplanNetDefinition *component; guint* ref_ptr; key = yaml_document_get_node(&npp->doc, entry->key); assert_type(npp, key, YAML_SCALAR_NODE); value = yaml_document_get_node(&npp->doc, entry->value); assert_type(npp, value, YAML_SCALAR_NODE); if (key_prefix && npp->null_fields) { g_autofree char* full_key = NULL; full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value); if (g_hash_table_contains(npp->null_fields, full_key)) continue; } component = g_hash_table_lookup(npp->parsed_defs, scalar(key)); if (!component) { add_missing_node(npp, key); } else { ref_ptr = ((guint*) ((void*) component + GPOINTER_TO_UINT(data))); if (*ref_ptr) return yaml_error(npp, node, error, "%s: interface '%s' already has a path cost of %u", npp->current.netdef->id, scalar(key), *ref_ptr); v = g_ascii_strtoull(scalar(value), &endptr, 10); if (*endptr != '\0') return yaml_error(npp, node, error, "invalid unsigned int value '%s'", scalar(value)); g_debug("%s: adding path '%s' of cost: %d", npp->current.netdef->id, scalar(key), v); *ref_ptr = v; mark_data_as_dirty(npp, ref_ptr); } } return TRUE; } STATIC gboolean handle_bridge_port_priority(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, const void* data, GError** error) { for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { yaml_node_t* key, *value; guint v; gchar* endptr; NetplanNetDefinition *component; guint* ref_ptr; key = yaml_document_get_node(&npp->doc, entry->key); assert_type(npp, key, YAML_SCALAR_NODE); value = yaml_document_get_node(&npp->doc, entry->value); assert_type(npp, value, YAML_SCALAR_NODE); if (key_prefix && npp->null_fields) { g_autofree char* full_key = NULL; full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value); if (g_hash_table_contains(npp->null_fields, full_key)) continue; } component = g_hash_table_lookup(npp->parsed_defs, scalar(key)); if (!component) { add_missing_node(npp, key); } else { ref_ptr = ((guint*) ((void*) component + GPOINTER_TO_UINT(data))); if (*ref_ptr) return yaml_error(npp, node, error, "%s: interface '%s' already has a port priority of %u", npp->current.netdef->id, scalar(key), *ref_ptr); v = g_ascii_strtoull(scalar(value), &endptr, 10); if (*endptr != '\0' || v > 63) return yaml_error(npp, node, error, "invalid port priority value (must be between 0 and 63): %s", scalar(value)); g_debug("%s: adding port '%s' of priority: %d", npp->current.netdef->id, scalar(key), v); *ref_ptr = v; mark_data_as_dirty(npp, ref_ptr); } } return TRUE; } static const mapping_entry_handler bridge_params_handlers[] = { {"ageing-time", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.ageing_time)}, {"aging-time", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.ageing_time)}, {"forward-delay", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.forward_delay)}, {"hello-time", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.hello_time)}, {"max-age", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.max_age)}, {"path-cost", YAML_MAPPING_NODE, {.map={.custom=handle_bridge_path_cost}}, netdef_offset(bridge_params.path_cost)}, {"port-priority", YAML_MAPPING_NODE, {.map={.custom=handle_bridge_port_priority}}, netdef_offset(bridge_params.port_priority)}, {"priority", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(bridge_params.priority)}, {"stp", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(bridge_params.stp)}, {NULL} }; STATIC gboolean handle_bridge(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, __unused const void* _, GError** error) { npp->current.netdef->custom_bridging = TRUE; npp->current.netdef->bridge_params.stp = TRUE; return process_mapping(npp, node, key_prefix, bridge_params_handlers, NULL, error); } /**************************************************** * Grammar and handlers for network config "routes" entry ****************************************************/ static const mapping_entry_handler routes_handlers[] = { {"from", YAML_SCALAR_NODE, {.generic=handle_routes_ip}, route_offset(from)}, {"on-link", YAML_SCALAR_NODE, {.generic=handle_routes_bool}, route_offset(onlink)}, {"scope", YAML_SCALAR_NODE, {.generic=handle_routes_scope}, NULL}, {"table", YAML_SCALAR_NODE, {.generic=handle_routes_guint}, route_offset(table)}, {"to", YAML_SCALAR_NODE, {.generic=handle_routes_destination}, NULL}, {"type", YAML_SCALAR_NODE, {.generic=handle_routes_type}, NULL}, {"via", YAML_SCALAR_NODE, {.generic=handle_routes_ip}, route_offset(via)}, {"metric", YAML_SCALAR_NODE, {.generic=handle_routes_guint}, route_offset(metric)}, {"mtu", YAML_SCALAR_NODE, {.generic=handle_routes_guint}, route_offset(mtubytes)}, {"congestion-window", YAML_SCALAR_NODE, {.generic=handle_routes_guint}, route_offset(congestion_window)}, {"advertised-receive-window", YAML_SCALAR_NODE, {.generic=handle_routes_guint}, route_offset(advertised_receive_window)}, {NULL} }; STATIC gboolean handle_routes(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { if (!npp->current.netdef->routes) npp->current.netdef->routes = g_array_new(FALSE, TRUE, sizeof(NetplanIPRoute*)); for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); NetplanIPRoute* route; assert_type(npp, entry, YAML_MAPPING_NODE); g_assert(npp->current.route == NULL); route = g_new0(NetplanIPRoute, 1); route->type = g_strdup("unicast"); route->scope = NULL; route->family = -1; /* 0 is a valid family ID */ route->metric = NETPLAN_METRIC_UNSPEC; /* 0 is a valid metric */ route->table = NETPLAN_ROUTE_TABLE_UNSPEC; g_debug("%s: adding new route", npp->current.netdef->id); npp->current.route = route; if (!process_mapping(npp, entry, NULL, routes_handlers, NULL, error)) goto err; /* Set the default scope, according to type */ if (!route->scope) { if ( g_ascii_strcasecmp(route->type, "local") == 0 || g_ascii_strcasecmp(route->type, "nat") == 0) route->scope = (g_strdup("host")); /* Non-gatewayed unicast routes are scope:link, too */ else if ( (g_ascii_strcasecmp(route->type, "unicast") == 0 && !route->via) || g_ascii_strcasecmp(route->type, "broadcast") == 0 || g_ascii_strcasecmp(route->type, "multicast") == 0 || g_ascii_strcasecmp(route->type, "anycast") == 0) route->scope = g_strdup("link"); else route->scope = g_strdup("global"); } if ( ( g_ascii_strcasecmp(route->scope, "link") == 0 || g_ascii_strcasecmp(route->scope, "host") == 0) && !route->to) { yaml_error(npp, node, error, "link and host routes must specify a 'to' IP"); goto err; } else if ( g_ascii_strcasecmp(route->type, "unicast") == 0 && g_ascii_strcasecmp(route->scope, "global") == 0 && (!route->to || !route->via)) { yaml_error(npp, node, error, "global unicast route must include both a 'to' and 'via' IP"); goto err; } else if (g_ascii_strcasecmp(route->type, "unicast") != 0 && !route->to) { yaml_error(npp, node, error, "non-unicast routes must specify a 'to' IP"); goto err; } if (is_route_present(npp->current.netdef, route)) { g_debug("%s: route (to: %s, via: %s, table: %d, metric: %d) has already been added", npp->current.netdef->id, route->to, route->via, route->table, route->metric); route_clear(&npp->current.route); npp->current.route = NULL; continue; } g_array_append_val(npp->current.netdef->routes, route); npp->current.route = NULL; } mark_data_as_dirty(npp, &npp->current.netdef->routes); return TRUE; err: route_clear(&npp->current.route); npp->current.route = NULL; return FALSE; } static const mapping_entry_handler ip_rules_handlers[] = { {"from", YAML_SCALAR_NODE, {.generic=handle_ip_rule_ip}, ip_rule_offset(from)}, {"mark", YAML_SCALAR_NODE, {.generic=handle_ip_rule_guint}, ip_rule_offset(fwmark)}, {"priority", YAML_SCALAR_NODE, {.generic=handle_ip_rule_guint}, ip_rule_offset(priority)}, {"table", YAML_SCALAR_NODE, {.generic=handle_ip_rule_guint}, ip_rule_offset(table)}, {"to", YAML_SCALAR_NODE, {.generic=handle_ip_rule_ip}, ip_rule_offset(to)}, {"type-of-service", YAML_SCALAR_NODE, {.generic=handle_ip_rule_tos}, ip_rule_offset(tos)}, {NULL} }; STATIC gboolean handle_ip_rules(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); gboolean ret; NetplanIPRule* ip_rule = g_new0(NetplanIPRule, 1); reset_ip_rule(ip_rule); npp->current.ip_rule = ip_rule; ret = process_mapping(npp, entry, NULL, ip_rules_handlers, NULL, error); npp->current.ip_rule = NULL; if (ret && !ip_rule->from && !ip_rule->to) ret = yaml_error(npp, node, error, "IP routing policy must include either a 'from' or 'to' IP"); if (!ret) { ip_rule_clear(&ip_rule); return FALSE; } if (!npp->current.netdef->ip_rules) npp->current.netdef->ip_rules = g_array_new(FALSE, FALSE, sizeof(NetplanIPRule*)); if (is_route_rule_present(npp->current.netdef, ip_rule)) { g_debug("%s: rule (from: %s, to: %s, table: %d) has already been added", npp->current.netdef->id, ip_rule->from, ip_rule->to, ip_rule->table); ip_rule_clear(&ip_rule); npp->current.ip_rule = NULL; continue; } g_array_append_val(npp->current.netdef->ip_rules, ip_rule); } mark_data_as_dirty(npp, &npp->current.netdef->ip_rules); return TRUE; } /**************************************************** * Grammar and handlers for bond parameters ****************************************************/ STATIC gboolean handle_arp_ip_targets(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { if (!npp->current.netdef->bond_params.arp_ip_targets) { npp->current.netdef->bond_params.arp_ip_targets = g_array_new(FALSE, FALSE, sizeof(char *)); } /* Avoid adding the same arp_ip_targets in a 2nd parsing pass by comparing * the array size to the YAML sequence size. Skip if they are equal. */ guint item_count = node->data.sequence.items.top - node->data.sequence.items.start; if (npp->current.netdef->bond_params.arp_ip_targets->len == item_count) { g_debug("%s: all arp ip targets have already been added", npp->current.netdef->id); return TRUE; } for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { g_autofree char* addr = NULL; yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); assert_type(npp, entry, YAML_SCALAR_NODE); addr = g_strdup(scalar(entry)); /* is it an IPv4 address? */ if (is_ip4_address(addr)) { char* s = g_strdup(scalar(entry)); g_array_append_val(npp->current.netdef->bond_params.arp_ip_targets, s); continue; } return yaml_error(npp, node, error, "malformed address '%s', must be X.X.X.X or X:X:X:X:X:X:X:X", scalar(entry)); } mark_data_as_dirty(npp, &npp->current.netdef->bond_params.arp_ip_targets); return TRUE; } STATIC gboolean handle_bond_primary_member(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { NetplanNetDefinition *component; char** ref_ptr; component = g_hash_table_lookup(npp->parsed_defs, scalar(node)); if (!component) { add_missing_node(npp, node); } else { /* If this is not the primary pass, the primary member might already be equally set. */ if (!g_strcmp0(npp->current.netdef->bond_params.primary_member, scalar(node))) { return TRUE; } else if (npp->current.netdef->bond_params.primary_member) return yaml_error(npp, node, error, "%s: bond already has a primary member: %s", npp->current.netdef->id, npp->current.netdef->bond_params.primary_member); ref_ptr = ((char**) ((void*) component + GPOINTER_TO_UINT(data))); *ref_ptr = g_strdup(scalar(node)); npp->current.netdef->bond_params.primary_member = g_strdup(scalar(node)); mark_data_as_dirty(npp, ref_ptr); } mark_data_as_dirty(npp, &npp->current.netdef->bond_params.primary_member); return TRUE; } STATIC gboolean handle_bond_lacp_rate(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (!(strcmp(scalar(node), "slow") == 0 || strcmp(scalar(node), "fast") == 0)) return yaml_error(npp, node, error, "unknown lacp-rate value '%s' (expected 'fast' or 'slow')", scalar(node)); return handle_netdef_str(npp, node, data, error); } static const mapping_entry_handler bond_params_handlers[] = { {"mode", YAML_SCALAR_NODE, {.generic=handle_bond_mode}, netdef_offset(bond_params.mode)}, {"lacp-rate", YAML_SCALAR_NODE, {.generic=handle_bond_lacp_rate}, netdef_offset(bond_params.lacp_rate)}, {"mii-monitor-interval", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.monitor_interval)}, {"min-links", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(bond_params.min_links)}, {"transmit-hash-policy", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.transmit_hash_policy)}, {"ad-select", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.selection_logic)}, {"all-slaves-active", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(bond_params.all_members_active)}, /* wokeignore:rule=slave */ {"all-members-active", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(bond_params.all_members_active)}, {"arp-interval", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.arp_interval)}, /* TODO: arp_ip_targets */ {"arp-ip-targets", YAML_SEQUENCE_NODE, {.generic=handle_arp_ip_targets}, NULL}, {"arp-validate", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.arp_validate)}, {"arp-all-targets", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.arp_all_targets)}, {"up-delay", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.up_delay)}, {"down-delay", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.down_delay)}, {"fail-over-mac-policy", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.fail_over_mac_policy)}, {"gratuitous-arp", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(bond_params.gratuitous_arp)}, /* Handle the old misspelling */ {"gratuitious-arp", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(bond_params.gratuitous_arp)}, /* TODO: unsolicited_na */ {"packets-per-slave", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(bond_params.packets_per_member)}, /* wokeignore:rule=slave */ {"packets-per-member", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(bond_params.packets_per_member)}, {"primary-reselect-policy", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.primary_reselect_policy)}, {"resend-igmp", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(bond_params.resend_igmp)}, {"learn-packet-interval", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bond_params.learn_interval)}, {"primary", YAML_SCALAR_NODE, {.generic=handle_bond_primary_member}, netdef_offset(bond_params.primary_member)}, {NULL} }; STATIC gboolean handle_vrf_interfaces(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { /* all entries must refer to already defined IDs */ for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); NetplanNetDefinition *component; assert_type(npp, entry, YAML_SCALAR_NODE); component = g_hash_table_lookup(npp->parsed_defs, scalar(entry)); if (!component) { add_missing_node(npp, entry); } else { if (component->vrf_link && component->vrf_link != npp->current.netdef) return yaml_error(npp, node, error, "%s: interface '%s' is already assigned to vrf %s", npp->current.netdef->id, scalar(entry), component->vrf_link->id); component->vrf_link = npp->current.netdef; } } return TRUE; } STATIC gboolean handle_vxlan_source_port(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { assert_type(npp, node, YAML_SEQUENCE_NODE); if (node->data.sequence.items.top - node->data.sequence.items.start != 2) return yaml_error(npp, node, error, "%s: Expected exactly two values for port-range", npp->current.netdef->id); yaml_node_t* itm1 = yaml_document_get_node(&npp->doc, *node->data.sequence.items.start); yaml_node_t* itm2 = yaml_document_get_node(&npp->doc, *node->data.sequence.items.start+1); if (!handle_generic_guint(npp, itm1, npp->current.vxlan, vxlan_offset(source_port_min), error)) return FALSE; if (!handle_generic_guint(npp, itm2, npp->current.vxlan, vxlan_offset(source_port_max), error)) return FALSE; guint tmp = 0; if (npp->current.netdef->vxlan->source_port_min > npp->current.netdef->vxlan->source_port_max) { tmp = npp->current.netdef->vxlan->source_port_min; npp->current.netdef->vxlan->source_port_min = npp->current.netdef->vxlan->source_port_max; npp->current.netdef->vxlan->source_port_max = tmp; g_warning("%s: swapped invalid port-range order [MIN, MAX]", npp->current.netdef->id); } return TRUE; } STATIC gboolean handle_bonding(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, __unused const void* _, GError** error) { return process_mapping(npp, node, key_prefix, bond_params_handlers, NULL, error); } STATIC gboolean handle_dhcp_identifier(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { g_free(npp->current.netdef->dhcp_identifier); /* "duid" is the default case, so we don't store it. */ if (g_ascii_strcasecmp(scalar(node), "duid") != 0) npp->current.netdef->dhcp_identifier = g_strdup(scalar(node)); else npp->current.netdef->dhcp_identifier = NULL; if (npp->current.netdef->dhcp_identifier == NULL || g_ascii_strcasecmp(npp->current.netdef->dhcp_identifier, "mac") == 0) return TRUE; return yaml_error(npp, node, error, "invalid DHCP client identifier type '%s'", npp->current.netdef->dhcp_identifier); } /**************************************************** * Grammar and handlers for tunnels ****************************************************/ STATIC gboolean handle_tunnel_addr(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_autofree char* addr = NULL; char* prefix_len; /* split off /prefix_len */ addr = g_strdup(scalar(node)); prefix_len = strrchr(addr, '/'); if (prefix_len) return yaml_error(npp, node, error, "address '%s' should not include /prefixlength", scalar(node)); /* is it an IPv4 address? */ if (is_ip4_address(addr)) return handle_netdef_ip4(npp, node, data, error); /* is it an IPv6 address? */ if (is_ip6_address(addr)) return handle_netdef_ip6(npp, node, data, error); return yaml_error(npp, node, error, "malformed address '%s', must be X.X.X.X or X:X:X:X:X:X:X:X", scalar(node)); } STATIC gboolean handle_tunnel_mode(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { const char *key = scalar(node); NetplanTunnelMode i; // Skip over unknown (0) tunnel mode. for (i = 1; i < NETPLAN_TUNNEL_MODE_MAX_; ++i) { if (g_strcmp0(netplan_tunnel_mode_name(i), key) == 0) { npp->current.netdef->tunnel.mode = i; return TRUE; } } return yaml_error(npp, node, error, "%s: tunnel mode '%s' is not supported", npp->current.netdef->id, key); } static const mapping_entry_handler tunnel_keys_handlers[] = { {"input", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(tunnel.input_key)}, {"output", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(tunnel.output_key)}, {"private", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(tunnel.private_key)}, {"private-key-flags", YAML_SEQUENCE_NODE, {.generic=handle_tunnel_key_flags}, NULL}, {NULL} }; STATIC gboolean handle_tunnel_key_mapping(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, __unused const void* _, GError** error) { gboolean ret = FALSE; /* We overload the 'key[s]' setting for tunnels; such that it can either be a * single scalar with the same key to use for both input, output and private * keys, or a mapping where one can specify each. */ if (node->type == YAML_SCALAR_NODE) { ret = handle_netdef_str(npp, node, netdef_offset(tunnel.input_key), error); if (ret) ret = handle_netdef_str(npp, node, netdef_offset(tunnel.output_key), error); if (ret) ret = handle_netdef_str(npp, node, netdef_offset(tunnel.private_key), error); } else if (node->type == YAML_MAPPING_NODE) ret = process_mapping(npp, node, key_prefix, tunnel_keys_handlers, NULL, error); else return yaml_error(npp, node, error, "invalid type for 'key[s]': must be a scalar or mapping"); return ret; } /** * Handler for setting a NetplanWireguardPeer string field from a scalar node * @data: pointer to the const char* field to write */ STATIC gboolean handle_wireguard_peer_str(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_assert(npp->current.wireguard_peer); return handle_generic_str(npp, node, npp->current.wireguard_peer, data, error); } /** * Handler for setting a NetplanWireguardPeer string field from a scalar node * @data: pointer to the guint field to write */ STATIC gboolean handle_wireguard_peer_guint(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { g_assert(npp->current.wireguard_peer); return handle_generic_guint(npp, node, npp->current.wireguard_peer, data, error); } STATIC gboolean handle_wireguard_allowed_ips(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { return handle_generic_addresses(npp, node, FALSE, &(npp->current.wireguard_peer->allowed_ips), &(npp->current.wireguard_peer->allowed_ips), error); } STATIC gboolean handle_wireguard_endpoint(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { g_autofree char* endpoint = NULL; char* port; char* address; guint64 port_num; /* If endpoint is an empty string just ignore it */ if (!g_strcmp0(scalar(node), "")) { return TRUE; } endpoint = g_strdup(scalar(node)); /* absolute minimal length of endpoint is 3 chars: 'h:8' */ if (strlen(endpoint) < 3) { return yaml_error(npp, node, error, "invalid endpoint address or hostname '%s'", scalar(node)); } if (endpoint[0] == '[') { /* this is an ipv6 endpoint in [ad:rr:ee::ss]:port form */ char *endbrace = strrchr(endpoint, ']'); if (!endbrace) return yaml_error(npp, node, error, "invalid address in endpoint '%s'", scalar(node)); address = endpoint + 1; *endbrace = '\0'; port = strrchr(endbrace + 1, ':'); } else { address = endpoint; port = strrchr(endpoint, ':'); } /* split off :port */ if (!port) return yaml_error(npp, node, error, "endpoint '%s' is missing :port", scalar(node)); *port = '\0'; port++; /* skip former ':' into first char of port */ port_num = g_ascii_strtoull(port, NULL, 10); if (port_num > 65535) return yaml_error(npp, node, error, "invalid port in endpoint '%s'", scalar(node)); if (is_ip4_address(address) || is_ip6_address(address) || is_hostname(address)) { return handle_wireguard_peer_str(npp, node, wireguard_peer_offset(endpoint), error); } return yaml_error(npp, node, error, "invalid endpoint address or hostname '%s'", scalar(node)); } static const mapping_entry_handler wireguard_peer_keys_handlers[] = { {"public", YAML_SCALAR_NODE, {.generic=handle_wireguard_peer_str}, wireguard_peer_offset(public_key)}, {"shared", YAML_SCALAR_NODE, {.generic=handle_wireguard_peer_str}, wireguard_peer_offset(preshared_key)}, {NULL} }; STATIC gboolean handle_wireguard_peer_key_mapping(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, __unused const void* _, GError** error) { return process_mapping(npp, node, key_prefix, wireguard_peer_keys_handlers, NULL, error); } const mapping_entry_handler wireguard_peer_handlers[] = { {"keys", YAML_MAPPING_NODE, {.map={.custom=handle_wireguard_peer_key_mapping}}, NULL}, {"keepalive", YAML_SCALAR_NODE, {.generic=handle_wireguard_peer_guint}, wireguard_peer_offset(keepalive)}, {"endpoint", YAML_SCALAR_NODE, {.generic=handle_wireguard_endpoint}, NULL}, {"allowed-ips", YAML_SEQUENCE_NODE, {.generic=handle_wireguard_allowed_ips}, NULL}, {NULL} }; STATIC gboolean handle_wireguard_peers(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { if (!npp->current.netdef->wireguard_peers) npp->current.netdef->wireguard_peers = g_array_new(FALSE, TRUE, sizeof(NetplanWireguardPeer*)); /* Avoid adding the same peers in a 2nd parsing pass by comparing * the array size to the YAML sequence size. Skip if they are equal. */ guint item_count = node->data.sequence.items.top - node->data.sequence.items.start; if (npp->current.netdef->wireguard_peers->len == item_count) { g_debug("%s: all wireguard peers have already been added", npp->current.netdef->id); return TRUE; } for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); assert_type(npp, entry, YAML_MAPPING_NODE); g_assert(npp->current.wireguard_peer == NULL); npp->current.wireguard_peer = g_new0(NetplanWireguardPeer, 1); npp->current.wireguard_peer->allowed_ips = g_array_new(FALSE, FALSE, sizeof(char*)); g_debug("%s: adding new wireguard peer", npp->current.netdef->id); if (!process_mapping(npp, entry, NULL, wireguard_peer_handlers, NULL, error)) { wireguard_peer_clear(&npp->current.wireguard_peer); npp->current.wireguard_peer = NULL; return FALSE; } g_array_append_val(npp->current.netdef->wireguard_peers, npp->current.wireguard_peer); npp->current.wireguard_peer = NULL; } return TRUE; } /**************************************************** * Grammar and handlers for network devices ****************************************************/ STATIC gboolean handle_ovs_bond_lacp(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (npp->current.netdef->type != NETPLAN_DEF_TYPE_BOND) return yaml_error(npp, node, error, "Key 'lacp' is only valid for interface type 'Open vSwitch bond'"); if (g_strcmp0(scalar(node), "active") && g_strcmp0(scalar(node), "passive") && g_strcmp0(scalar(node), "off")) return yaml_error(npp, node, error, "Value of 'lacp' needs to be 'active', 'passive' or 'off"); return handle_netdef_str(npp, node, data, error); } STATIC gboolean handle_ovs_bridge_bool(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (npp->current.netdef->type != NETPLAN_DEF_TYPE_BRIDGE) return yaml_error(npp, node, error, "Key is only valid for interface type 'Open vSwitch bridge'"); return handle_netdef_bool(npp, node, data, error); } STATIC gboolean handle_ovs_bridge_fail_mode(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (npp->current.netdef->type != NETPLAN_DEF_TYPE_BRIDGE) return yaml_error(npp, node, error, "Key 'fail-mode' is only valid for interface type 'Open vSwitch bridge'"); if (g_strcmp0(scalar(node), "standalone") && g_strcmp0(scalar(node), "secure")) return yaml_error(npp, node, error, "Value of 'fail-mode' needs to be 'standalone' or 'secure'"); return handle_netdef_str(npp, node, data, error); } STATIC gboolean handle_ovs_protocol(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, GError** error) { const char* deprecated[] = { "OpenFlow16" }; const char* supported[] = { "OpenFlow10", "OpenFlow11", "OpenFlow12", "OpenFlow13", "OpenFlow14", "OpenFlow15", NULL }; unsigned i = 0; guint offset = GPOINTER_TO_UINT(data); GArray** protocols = (GArray**) ((void*) entryptr + offset); for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) { yaml_node_t *entry = yaml_document_get_node(&npp->doc, *iter); assert_type(npp, entry, YAML_SCALAR_NODE); if (!g_strcmp0(scalar(entry), deprecated[0])) { g_warning("Open vSwitch: Ignoring deprecated protocol: %s", scalar(entry)); continue; } for (i = 0; supported[i] != NULL; ++i) if (!g_strcmp0(scalar(entry), supported[i])) break; if (supported[i] == NULL) return yaml_error(npp, node, error, "Unsupported OVS 'protocol' value: %s", scalar(entry)); if (!*protocols) *protocols = g_array_new(FALSE, FALSE, sizeof(char*)); /* Do not insert the same address twice in the list */ if (!is_string_in_array(*protocols, scalar(entry))) { char* s = g_strdup(scalar(entry)); g_array_append_val(*protocols, s); } } return TRUE; } STATIC gboolean handle_ovs_bridge_protocol(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (npp->current.netdef->type != NETPLAN_DEF_TYPE_BRIDGE) return yaml_error(npp, node, error, "Key 'protocols' is only valid for interface type 'Open vSwitch bridge'"); return handle_ovs_protocol(npp, node, npp->current.netdef, data, error); } STATIC gboolean handle_ovs_bridge_controller_connection_mode(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { if (npp->current.netdef->type != NETPLAN_DEF_TYPE_BRIDGE) return yaml_error(npp, node, error, "Key 'controller.connection-mode' is only valid for interface type 'Open vSwitch bridge'"); if (g_strcmp0(scalar(node), "in-band") && g_strcmp0(scalar(node), "out-of-band")) return yaml_error(npp, node, error, "Value of 'connection-mode' needs to be 'in-band' or 'out-of-band'"); return handle_netdef_str(npp, node, data, error); } STATIC gboolean handle_ovs_bridge_controller_addresses(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { if (npp->current.netdef->type != NETPLAN_DEF_TYPE_BRIDGE) return yaml_error(npp, node, error, "Key 'controller.addresses' is only valid for interface type 'Open vSwitch bridge'"); for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { gchar** vec = NULL; gboolean is_host = FALSE; gboolean is_port = FALSE; gboolean is_unix = FALSE; yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); assert_type(npp, entry, YAML_SCALAR_NODE); /* We always need at least one colon */ if (!g_strrstr(scalar(entry), ":")) return yaml_error(npp, node, error, "Unsupported OVS controller target: %s", scalar(entry)); vec = g_strsplit (scalar(entry), ":", 2); is_host = !g_strcmp0(vec[0], "tcp") || !g_strcmp0(vec[0], "ssl"); is_port = !g_strcmp0(vec[0], "ptcp") || !g_strcmp0(vec[0], "pssl"); is_unix = !g_strcmp0(vec[0], "unix") || !g_strcmp0(vec[0], "punix"); if (!npp->current.netdef->ovs_settings.controller.addresses) npp->current.netdef->ovs_settings.controller.addresses = g_array_new(FALSE, FALSE, sizeof(char*)); /* Do not insert the same address twice in the list */ if (is_string_in_array(npp->current.netdef->ovs_settings.controller.addresses, scalar(entry))) { g_strfreev(vec); continue; } /* Format: [p]unix:file */ if (is_unix && vec[1] != NULL && vec[2] == NULL) { char* s = g_strdup(scalar(entry)); g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s); g_strfreev(vec); continue; /* Format tcp:host[:port] or ssl:host[:port] */ } else if (is_host && validate_ovs_target(TRUE, vec[1])) { char* s = g_strdup(scalar(entry)); g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s); g_strfreev(vec); continue; /* Format ptcp:[port][:host] or pssl:[port][:host] */ } else if (is_port && validate_ovs_target(FALSE, vec[1])) { char* s = g_strdup(scalar(entry)); g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s); g_strfreev(vec); continue; } g_strfreev(vec); return yaml_error(npp, node, error, "Unsupported OVS controller target: %s", scalar(entry)); } return TRUE; } static const mapping_entry_handler ovs_controller_handlers[] = { {"addresses", YAML_SEQUENCE_NODE, {.generic=handle_ovs_bridge_controller_addresses}, netdef_offset(ovs_settings.controller.addresses)}, {"connection-mode", YAML_SCALAR_NODE, {.generic=handle_ovs_bridge_controller_connection_mode}, netdef_offset(ovs_settings.controller.connection_mode)}, {NULL}, }; static const mapping_entry_handler ovs_backend_settings_handlers[] = { {"external-ids", YAML_MAPPING_NODE, {.map={.custom=handle_netdef_map}}, netdef_offset(ovs_settings.external_ids)}, {"other-config", YAML_MAPPING_NODE, {.map={.custom=handle_netdef_map}}, netdef_offset(ovs_settings.other_config)}, {"lacp", YAML_SCALAR_NODE, {.generic=handle_ovs_bond_lacp}, netdef_offset(ovs_settings.lacp)}, {"fail-mode", YAML_SCALAR_NODE, {.generic=handle_ovs_bridge_fail_mode}, netdef_offset(ovs_settings.fail_mode)}, {"mcast-snooping", YAML_SCALAR_NODE, {.generic=handle_ovs_bridge_bool}, netdef_offset(ovs_settings.mcast_snooping)}, {"rstp", YAML_SCALAR_NODE, {.generic=handle_ovs_bridge_bool}, netdef_offset(ovs_settings.rstp)}, {"protocols", YAML_SEQUENCE_NODE, {.generic=handle_ovs_bridge_protocol}, netdef_offset(ovs_settings.protocols)}, {"controller", YAML_MAPPING_NODE, {.map={.handlers=ovs_controller_handlers}}, NULL}, {NULL} }; STATIC gboolean handle_ovs_backend(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, __unused const void* _, GError** error) { GList* values = NULL; gboolean ret = process_mapping(npp, node, key_prefix, ovs_backend_settings_handlers, &values, error); guint len = g_list_length(values); if (npp->current.netdef->type != NETPLAN_DEF_TYPE_BOND && npp->current.netdef->type != NETPLAN_DEF_TYPE_BRIDGE) { GList *other_config = g_list_find_custom(values, "other-config", (GCompareFunc) strcmp); GList *external_ids = g_list_find_custom(values, "external-ids", (GCompareFunc) strcmp); /* Non-bond/non-bridge interfaces might still be handled by the networkd backend */ if (len == 1 && (other_config || external_ids)) goto cleanup; else if (len == 2 && other_config && external_ids) goto cleanup; } /* Set the renderer for this device to NETPLAN_BACKEND_OVS, implicitly. * But only if empty "openvswitch: {}" or "openvswitch:" with more than * "other-config" or "external-ids" keys is given. */ npp->current.netdef->backend = NETPLAN_BACKEND_OVS; cleanup: g_list_free_full(values, g_free); return ret; } static const mapping_entry_handler nameservers_handlers[] = { {"search", YAML_SEQUENCE_NODE, {.generic=handle_nameservers_search}, NULL}, {"addresses", YAML_SEQUENCE_NODE, {.generic=handle_nameservers_addresses}, NULL}, {NULL} }; /* Handlers for DHCP overrides. */ #define COMMON_DHCP_OVERRIDES_HANDLERS(overrides) \ {"hostname", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(overrides.hostname)}, \ {"route-metric", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(overrides.metric)}, \ {"send-hostname", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(overrides.send_hostname)}, \ {"use-dns", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(overrides.use_dns)}, \ {"use-domains", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(overrides.use_domains)}, \ {"use-hostname", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(overrides.use_hostname)}, \ {"use-mtu", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(overrides.use_mtu)}, \ {"use-ntp", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(overrides.use_ntp)}, \ {"use-routes", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(overrides.use_routes)} static const mapping_entry_handler dhcp4_overrides_handlers[] = { COMMON_DHCP_OVERRIDES_HANDLERS(dhcp4_overrides), {NULL}, }; static const mapping_entry_handler dhcp6_overrides_handlers[] = { COMMON_DHCP_OVERRIDES_HANDLERS(dhcp6_overrides), {NULL}, }; /* Handlers shared by all link types */ #define COMMON_LINK_HANDLERS \ {"accept-ra", YAML_SCALAR_NODE, {.generic=handle_accept_ra}, netdef_offset(accept_ra)}, \ {"activation-mode", YAML_SCALAR_NODE, {.generic=handle_activation_mode}, netdef_offset(activation_mode)}, \ {"addresses", YAML_SEQUENCE_NODE, {.generic=handle_addresses}, NULL}, \ {"critical", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(critical)}, \ {"ignore-carrier", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(ignore_carrier)}, \ {"dhcp4", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(dhcp4)}, \ {"dhcp6", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(dhcp6)}, \ {"dhcp-identifier", YAML_SCALAR_NODE, {.generic=handle_dhcp_identifier}, NULL}, \ {"dhcp4-overrides", YAML_MAPPING_NODE, {.map={.handlers=dhcp4_overrides_handlers}}, NULL}, \ {"dhcp6-overrides", YAML_MAPPING_NODE, {.map={.handlers=dhcp6_overrides_handlers}}, NULL}, \ {"gateway4", YAML_SCALAR_NODE, {.generic=handle_gateway4}, NULL}, \ {"gateway6", YAML_SCALAR_NODE, {.generic=handle_gateway6}, NULL}, \ {"ipv6-address-generation", YAML_SCALAR_NODE, {.generic=handle_netdef_addrgen}, NULL}, \ {"ipv6-address-token", YAML_SCALAR_NODE, {.generic=handle_netdef_addrtok}, netdef_offset(ip6_addr_gen_token)}, \ {"ipv6-mtu", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(ipv6_mtubytes)}, \ {"ipv6-privacy", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(ip6_privacy)}, \ {"link-local", YAML_SEQUENCE_NODE, {.generic=handle_link_local}, NULL}, \ {"macaddress", YAML_SCALAR_NODE, {.generic=handle_netdef_set_mac}, netdef_offset(set_mac)}, \ {"mtu", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(mtubytes)}, \ {"nameservers", YAML_MAPPING_NODE, {.map={.handlers=nameservers_handlers}}, NULL}, \ {"optional", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(optional)}, \ {"optional-addresses", YAML_SEQUENCE_NODE, {.generic=handle_optional_addresses}, NULL}, \ {"renderer", YAML_SCALAR_NODE, {.generic=handle_netdef_renderer}, NULL}, \ {"routes", YAML_SEQUENCE_NODE, {.generic=handle_routes}, NULL}, \ {"routing-policy", YAML_SEQUENCE_NODE, {.generic=handle_ip_rules}, NULL}, \ {"hairpin", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(bridge_hairpin)}, \ {"port-mac-learning", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(bridge_learning)}, \ {"neigh-suppress", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(bridge_neigh_suppress)} #define COMMON_BACKEND_HANDLERS \ {"networkmanager", YAML_MAPPING_NODE, {.map={.handlers=nm_backend_settings_handlers}}, NULL}, \ {"openvswitch", YAML_MAPPING_NODE, {.map={.custom=handle_ovs_backend}}, NULL} /* Handlers for physical links */ #define PHYSICAL_LINK_HANDLERS \ {"match", YAML_MAPPING_NODE, {.map={.custom=handle_match}}, NULL}, \ {"set-name", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(set_name)}, \ {"wakeonlan", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(wake_on_lan)}, \ {"wakeonwlan", YAML_SEQUENCE_NODE, {.generic=handle_wowlan}, netdef_offset(wowlan)}, \ {"emit-lldp", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(emit_lldp)}, \ {"receive-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(receive_checksum_offload)}, \ {"transmit-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(transmit_checksum_offload)}, \ {"tcp-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(tcp_segmentation_offload)}, \ {"tcp6-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(tcp6_segmentation_offload)}, \ {"generic-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(generic_segmentation_offload)}, \ {"generic-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(generic_receive_offload)}, \ {"large-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(large_receive_offload)} static const mapping_entry_handler ethernet_def_handlers[] = { COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, PHYSICAL_LINK_HANDLERS, {"auth", YAML_MAPPING_NODE, {.map={.custom=handle_auth}}, NULL}, {"link", YAML_SCALAR_NODE, {.generic=handle_netdef_id_ref}, netdef_offset(sriov_link)}, {"virtual-function-count", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(sriov_explicit_vf_count)}, {"embedded-switch-mode", YAML_SCALAR_NODE, {.generic=handle_embedded_switch_mode}, netdef_offset(embedded_switch_mode)}, {"delay-virtual-functions-rebind", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(sriov_delay_virtual_functions_rebind)}, {"infiniband-mode", YAML_SCALAR_NODE, {.generic=handle_ib_mode}, netdef_offset(ib_mode)}, {NULL} }; static const mapping_entry_handler veth_def_handlers[] = { COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, {"peer", YAML_SCALAR_NODE, {.generic=handle_veth_peer}, netdef_offset(veth_peer_link)}, {NULL} }; static const mapping_entry_handler wifi_def_handlers[] = { COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, PHYSICAL_LINK_HANDLERS, {"access-points", YAML_MAPPING_NODE, {.map={.custom=handle_wifi_access_points}}, NULL}, {"auth", YAML_MAPPING_NODE, {.map={.custom=handle_auth}}, NULL}, {"regulatory-domain", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(regulatory_domain)}, {NULL} }; static const mapping_entry_handler bridge_def_handlers[] = { COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, {"interfaces", YAML_SEQUENCE_NODE, {.generic=handle_bridge_interfaces}, NULL}, {"parameters", YAML_MAPPING_NODE, {.map={.custom=handle_bridge}}, NULL}, {NULL} }; static const mapping_entry_handler bond_def_handlers[] = { COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, {"interfaces", YAML_SEQUENCE_NODE, {.generic=handle_bond_interfaces}, NULL}, {"parameters", YAML_MAPPING_NODE, {.map={.custom=handle_bonding}}, NULL}, {NULL} }; static const mapping_entry_handler vlan_def_handlers[] = { COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, {"id", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(vlan_id)}, {"link", YAML_SCALAR_NODE, {.generic=handle_netdef_id_ref}, netdef_offset(vlan_link)}, {NULL} }; static const mapping_entry_handler vrf_def_handlers[] = { COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, {"renderer", YAML_SCALAR_NODE, {.generic=handle_netdef_renderer}, NULL}, {"interfaces", YAML_SEQUENCE_NODE, {.generic=handle_vrf_interfaces}, NULL}, {"table", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(vrf_table)}, {"routes", YAML_SEQUENCE_NODE, {.generic=handle_routes}, NULL}, {"routing-policy", YAML_SEQUENCE_NODE, {.generic=handle_ip_rules}, NULL}, {NULL} }; static const mapping_entry_handler modem_def_handlers[] = { COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, PHYSICAL_LINK_HANDLERS, {"apn", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(modem_params.apn)}, {"auto-config", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(modem_params.auto_config)}, {"device-id", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(modem_params.device_id)}, {"network-id", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(modem_params.network_id)}, {"number", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(modem_params.number)}, {"password", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(modem_params.password)}, {"pin", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(modem_params.pin)}, {"sim-id", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(modem_params.sim_id)}, {"sim-operator-id", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(modem_params.sim_operator_id)}, {"username", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(modem_params.username)}, }; static const mapping_entry_handler dummy_def_handlers[] = { /* wokeignore:rule=dummy */ COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, {NULL} }; static const mapping_entry_handler tunnel_def_handlers[] = { COMMON_LINK_HANDLERS, COMMON_BACKEND_HANDLERS, {"mode", YAML_SCALAR_NODE, {.generic=handle_tunnel_mode}, NULL}, {"local", YAML_SCALAR_NODE, {.generic=handle_tunnel_addr}, netdef_offset(tunnel.local_ip)}, {"remote", YAML_SCALAR_NODE, {.generic=handle_tunnel_addr}, netdef_offset(tunnel.remote_ip)}, {"ttl", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(tunnel_ttl)}, /* Handle key/keys for clarity in config: this can be either a scalar or * mapping of multiple keys (input and output) */ {"key", YAML_NO_NODE, {.variable=handle_tunnel_key_mapping}, NULL}, {"keys", YAML_NO_NODE, {.variable=handle_tunnel_key_mapping}, NULL}, /* wireguard */ {"mark", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(tunnel.fwmark)}, {"port", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(tunnel.port)}, {"peers", YAML_SEQUENCE_NODE, {.generic=handle_wireguard_peers}, NULL}, /* vxlan */ {"link", YAML_SCALAR_NODE, {.generic=handle_vxlan_id_ref}, vxlan_offset(link)}, {"ageing", YAML_SCALAR_NODE, {.generic=handle_vxlan_guint}, vxlan_offset(ageing)}, {"aging", YAML_SCALAR_NODE, {.generic=handle_vxlan_guint}, vxlan_offset(ageing)}, {"id", YAML_SCALAR_NODE, {.generic=handle_vxlan_guint}, vxlan_offset(vni)}, {"limit", YAML_SCALAR_NODE, {.generic=handle_vxlan_guint}, vxlan_offset(limit)}, {"type-of-service", YAML_SCALAR_NODE, {.generic=handle_vxlan_guint}, vxlan_offset(tos)}, {"flow-label", YAML_SCALAR_NODE, {.generic=handle_vxlan_guint}, vxlan_offset(flow_label)}, {"do-not-fragment", YAML_SCALAR_NODE, {.generic=handle_vxlan_tristate}, vxlan_offset(do_not_fragment)}, {"short-circuit", YAML_SCALAR_NODE, {.generic=handle_vxlan_tristate}, vxlan_offset(short_circuit)}, {"arp-proxy", YAML_SCALAR_NODE, {.generic=handle_vxlan_tristate}, vxlan_offset(arp_proxy)}, {"mac-learning", YAML_SCALAR_NODE, {.generic=handle_vxlan_tristate}, vxlan_offset(mac_learning)}, {"notifications", YAML_SEQUENCE_NODE, {.generic=handle_vxlan_flags}, vxlan_offset(notifications)}, {"checksums", YAML_SEQUENCE_NODE, {.generic=handle_vxlan_flags}, vxlan_offset(checksums)}, {"extensions", YAML_SEQUENCE_NODE, {.generic=handle_vxlan_flags}, vxlan_offset(extensions)}, {"port-range", YAML_SEQUENCE_NODE, {.generic=handle_vxlan_source_port}, NULL}, {NULL} }; /**************************************************** * Grammar and handlers for network node ****************************************************/ STATIC gboolean handle_network_version(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { long mangled_version; mangled_version = strtol(scalar(node), NULL, 10); if (mangled_version < NETPLAN_VERSION_MIN || mangled_version >= NETPLAN_VERSION_MAX) return yaml_error(npp, node, error, "Only version 2 is supported"); return TRUE; } STATIC gboolean handle_network_renderer(NetplanParser* npp, yaml_node_t* node, __unused const void* _, GError** error) { gboolean res = parse_renderer(npp, node, &npp->global_backend, error); if (!npp->global_renderer) npp->global_renderer = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); char* key = npp->current.filepath ? g_strdup(npp->current.filepath) : g_strdup(""); /* Track the global renderer value of the current file. * If current.filepath is empty, this YAML is parsed from an unnamed YAML * patch (e.g. via 'netplan set '). */ g_hash_table_insert(npp->global_renderer, key, GINT_TO_POINTER(npp->global_backend)); return res; } STATIC gboolean handle_network_ovs_settings_global(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, const void* data, GError** error) { return handle_generic_map(npp, node, key_prefix, &npp->global_ovs_settings, data, error); } STATIC gboolean handle_network_ovs_settings_global_protocol(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_ovs_protocol(npp, node, &npp->global_ovs_settings, data, error); } STATIC gboolean handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) { yaml_node_t* port = NULL; yaml_node_t* peer = NULL; yaml_node_t* pair = NULL; yaml_node_item_t *item = NULL; NetplanNetDefinition *component1 = NULL; NetplanNetDefinition *component2 = NULL; for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) { pair = yaml_document_get_node(&npp->doc, *iter); assert_type(npp, pair, YAML_SEQUENCE_NODE); item = pair->data.sequence.items.start; /* A peer port definition must contain exactly 2 ports */ if (item+2 != pair->data.sequence.items.top) { return yaml_error(npp, pair, error, "An Open vSwitch peer port sequence must have exactly two entries"); } port = yaml_document_get_node(&npp->doc, *item); assert_type(npp, port, YAML_SCALAR_NODE); peer = yaml_document_get_node(&npp->doc, *(item+1)); assert_type(npp, peer, YAML_SCALAR_NODE); if (!g_strcmp0(scalar(port), scalar(peer))) return yaml_error(npp, peer, error, "Open vSwitch patch ports must be of different name"); /* Create port 1 netdef */ component1 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, scalar(port)) : NULL; if (!component1) { component1 = netplan_netdef_new(npp, scalar(port), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS); if (g_hash_table_remove(npp->missing_id, scalar(port))) npp->missing_ids_found++; } if (npp->current.filepath) { if (component1->filepath) g_free(component1->filepath); component1->filepath = g_strdup(npp->current.filepath); } if (component1->peer && g_strcmp0(component1->peer, scalar(peer))) return yaml_error(npp, port, error, "Open vSwitch port '%s' is already assigned to peer '%s'", component1->id, component1->peer); /* Create port 2 (peer) netdef */ component2 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, scalar(peer)) : NULL; if (!component2) { component2 = netplan_netdef_new(npp, scalar(peer), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS); if (g_hash_table_remove(npp->missing_id, scalar(peer))) npp->missing_ids_found++; } if (npp->current.filepath) { if (component2->filepath) g_free(component2->filepath); component2->filepath = g_strdup(npp->current.filepath); } if (component2->peer && g_strcmp0(component2->peer, scalar(port))) return yaml_error(npp, peer, error, "Open vSwitch port '%s' is already assigned to peer '%s'", component2->id, component2->peer); if (!component1->peer) { component1->peer = g_strdup(scalar(peer)); component1->peer_link = component2; } if (!component2->peer) { component2->peer = g_strdup(scalar(port)); component2->peer_link = component1; } } return TRUE; } STATIC gboolean node_is_nulled_out(yaml_document_t* doc, yaml_node_t* node, const char* key_prefix, GHashTable* null_fields) { if (node->type != YAML_MAPPING_NODE) return FALSE; // Empty nodes are not nulled-out, they're just empty! if (node->data.mapping.pairs.start == node->data.mapping.pairs.top) return FALSE; for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { yaml_node_t* key, *value; g_autofree char* full_key = NULL; key = yaml_document_get_node(doc, entry->key); value = yaml_document_get_node(doc, entry->value); full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value); // null detected, so we now flip the default return. if (g_hash_table_contains(null_fields, full_key)) continue; if (!node_is_nulled_out(doc, value, full_key, null_fields)) return FALSE; } return TRUE; } /** * Callback for a net device type entry like "ethernets:" in "network:" * @data: netdef_type (as pointer) */ STATIC gboolean handle_network_type(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, const void* data, GError** error) { for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { yaml_node_t* key, *value; const mapping_entry_handler* handlers; g_autofree char* full_key = NULL; key = yaml_document_get_node(&npp->doc, entry->key); if (!assert_valid_id(npp, key, error)) return FALSE; /* globbing is not allowed for IDs */ if (strpbrk(scalar(key), "*[]?")) return yaml_error(npp, key, error, "Definition ID '%s' must not use globbing", scalar(key)); value = yaml_document_get_node(&npp->doc, entry->value); if (key_prefix && (npp->null_fields || npp->null_overrides)) { full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value); /* Ignore NULL fields (about to be deleted) */ if (npp->null_fields && (g_hash_table_contains(npp->null_fields, full_key) || node_is_nulled_out(&npp->doc, value, full_key, npp->null_fields))) continue; /* Ignore this netdef if it is supposed to be part of the resulting * origin-hint file, but we're not currently processing said filepath. */ if (npp->null_overrides) { const gchar* origin_hint = g_hash_table_lookup(npp->null_overrides, full_key); g_autofree gchar* basename = npp->current.filepath ? g_path_get_basename(npp->current.filepath) : NULL; if (origin_hint && basename && g_strcmp0(origin_hint, basename) != 0) continue; } } /* special-case "renderer:" key to set the per-type backend */ if (strcmp(scalar(key), "renderer") == 0) { if (!parse_renderer(npp, value, &npp->current.backend, error)) return FALSE; continue; } assert_type(npp, value, YAML_MAPPING_NODE); /* At this point we've seen a new starting definition, if it has been * already mentioned in another netdef, removing it from our "missing" * list. */ if(g_hash_table_remove(npp->missing_id, scalar(key))) npp->missing_ids_found++; npp->current.netdef = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, scalar(key)) : NULL; if (npp->current.netdef) { /* already exists, overriding/amending previous definition */ if (npp->current.netdef->type != GPOINTER_TO_UINT(data)) { /* If the existing netdef is a place holder, we just repurpose it */ if (npp->current.netdef->type == NETPLAN_DEF_TYPE_NM_PLACEHOLDER_) npp->current.netdef->type = GPOINTER_TO_UINT(data); else return yaml_error(npp, key, error, "Updated definition '%s' changes device type", scalar(key)); } } else { npp->current.netdef = netplan_netdef_new(npp, scalar(key), GPOINTER_TO_UINT(data), npp->current.backend); } if (npp->current.filepath) { if (npp->current.netdef->filepath) g_free(npp->current.netdef->filepath); npp->current.netdef->filepath = g_strdup(npp->current.filepath); } // XXX: breaks multi-pass parsing. //if (!g_hash_table_add(ids_in_file, npp->current.netdef->id)) // return yaml_error(npp, key, error, "Duplicate net definition ID '%s'", npp->current.netdef->id); /* and fill it with definitions */ switch (npp->current.netdef->type) { case NETPLAN_DEF_TYPE_BOND: handlers = bond_def_handlers; break; case NETPLAN_DEF_TYPE_BRIDGE: handlers = bridge_def_handlers; break; case NETPLAN_DEF_TYPE_ETHERNET: handlers = ethernet_def_handlers; break; case NETPLAN_DEF_TYPE_MODEM: handlers = modem_def_handlers; break; case NETPLAN_DEF_TYPE_TUNNEL: handlers = tunnel_def_handlers; break; case NETPLAN_DEF_TYPE_VLAN: handlers = vlan_def_handlers; break; case NETPLAN_DEF_TYPE_VRF: handlers = vrf_def_handlers; break; case NETPLAN_DEF_TYPE_WIFI: handlers = wifi_def_handlers; break; case NETPLAN_DEF_TYPE_DUMMY: handlers = dummy_def_handlers; break; /* wokeignore:rule=dummy */ case NETPLAN_DEF_TYPE_VETH: handlers = veth_def_handlers; break; case NETPLAN_DEF_TYPE_NM: g_debug("netplan: %s: handling NetworkManager passthrough device, settings are not fully supported.", npp->current.netdef->id); handlers = ethernet_def_handlers; if (npp->current.netdef->backend != NETPLAN_BACKEND_NM) { g_warning("nm-device: %s: the renderer for nm-devices must be NetworkManager, it will be used instead of the defined one.", npp->current.netdef->id); npp->current.netdef->backend = NETPLAN_BACKEND_NM; } break; default: g_assert_not_reached(); // LCOV_EXCL_LINE } /* Preprocessing */ /* Any tunnel netdef needs to carry the 'vxlan' struct, as it might * potentially be a VXLAN tunnel. */ if (npp->current.netdef->type == NETPLAN_DEF_TYPE_TUNNEL) { NetplanVxlan* vxlan = g_new0(NetplanVxlan, 1); reset_vxlan(vxlan); npp->current.vxlan = vxlan; if (npp->current.netdef->vxlan) g_free(npp->current.netdef->vxlan); npp->current.netdef->vxlan = vxlan; } if (!process_mapping(npp, value, full_key, handlers, NULL, error)) return FALSE; /* Postprocessing */ /* Implicit VXLAN settings, which can be deduced from parsed data. */ if (npp->current.netdef->type == NETPLAN_DEF_TYPE_TUNNEL && npp->current.netdef->tunnel.mode == NETPLAN_TUNNEL_MODE_VXLAN) { if (npp->current.netdef->vxlan->link) npp->current.netdef->vxlan->link->has_vxlans = TRUE; else npp->current.netdef->vxlan->independent = TRUE; } /* validate definition-level conditions */ if (!validate_netdef_grammar(npp, npp->current.netdef, error)) return FALSE; /* convenience shortcut: physical device without match: means match * name on ID */ if (npp->current.netdef->type < NETPLAN_DEF_TYPE_VIRTUAL && !npp->current.netdef->has_match) set_str_if_null(npp->current.netdef->match.original_name, npp->current.netdef->id); } npp->current.backend = NETPLAN_BACKEND_NONE; return TRUE; } static const mapping_entry_handler ovs_global_ssl_handlers[] = { {"ca-cert", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(ca_certificate)}, {"certificate", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(client_certificate)}, {"private-key", YAML_SCALAR_NODE, {.generic=handle_auth_str}, auth_offset(client_key)}, {NULL} }; STATIC gboolean handle_ovs_global_ssl(NetplanParser* npp, yaml_node_t* node, const char* key_prefix, __unused const void* _, GError** error) { gboolean ret; npp->current.auth = &(npp->global_ovs_settings.ssl); ret = process_mapping(npp, node, key_prefix, ovs_global_ssl_handlers, NULL, error); npp->current.auth = NULL; return ret; } static const mapping_entry_handler ovs_network_settings_handlers[] = { {"external-ids", YAML_MAPPING_NODE, {.map={.custom=handle_network_ovs_settings_global}}, ovs_settings_offset(external_ids)}, {"other-config", YAML_MAPPING_NODE, {.map={.custom=handle_network_ovs_settings_global}}, ovs_settings_offset(other_config)}, {"protocols", YAML_SEQUENCE_NODE, {.generic=handle_network_ovs_settings_global_protocol}, ovs_settings_offset(protocols)}, {"ports", YAML_SEQUENCE_NODE, {.generic=handle_network_ovs_settings_global_ports}, NULL}, {"ssl", YAML_MAPPING_NODE, {.map={.custom=handle_ovs_global_ssl}}, NULL}, {NULL} }; static const mapping_entry_handler network_handlers[] = { {"bonds", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_BOND)}, {"bridges", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_BRIDGE)}, {"ethernets", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_ETHERNET)}, {"renderer", YAML_SCALAR_NODE, {.generic=handle_network_renderer}, NULL}, {"tunnels", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_TUNNEL)}, {"version", YAML_SCALAR_NODE, {.generic=handle_network_version}, NULL}, {"vlans", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_VLAN)}, {"vrfs", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_VRF)}, {"wifis", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_WIFI)}, {"modems", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_MODEM)}, {"dummy-devices", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_DUMMY)}, /* wokeignore:rule=dummy */ {"virtual-ethernets", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_VETH)}, {"nm-devices", YAML_MAPPING_NODE, {.map={.custom=handle_network_type}}, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_NM)}, {"openvswitch", YAML_MAPPING_NODE, {.map={.handlers=ovs_network_settings_handlers}}, NULL}, {NULL} }; /**************************************************** * Grammar and handlers for root node ****************************************************/ static const mapping_entry_handler root_handlers[] = { {"network", YAML_MAPPING_NODE, {.map={.handlers=network_handlers}}, NULL}, {NULL} }; /* * Post-process some specific missing interfaces that are not required * to exist but are needed in order to generate backend configuration. */ STATIC void process_missing_ids(NetplanParser* npp, __unused GError** error) { GHashTableIter iter; gpointer key, value; if (g_hash_table_size(npp->missing_id) == 0) return; g_hash_table_iter_init(&iter, npp->missing_id); while (g_hash_table_iter_next(&iter, &key, &value)) { NetplanMissingNode* missing = (NetplanMissingNode*) value; NetplanNetDefinition* netdef = g_hash_table_lookup(npp->parsed_defs, missing->netdef_id); NetplanBackend backend = netdef->backend != NETPLAN_BACKEND_NONE ? netdef->backend : npp->global_backend; /* VLAN case: NetworkManager doesn't enforce the existence of a parent interface in order to * create a VLAN. */ if (netdef->type == NETPLAN_DEF_TYPE_VLAN && backend == NETPLAN_BACKEND_NM) { netdef->vlan_link = netplan_netdef_new(npp, scalar(missing->node), NETPLAN_DEF_TYPE_NM_PLACEHOLDER_, NETPLAN_BACKEND_NM); g_hash_table_iter_remove(&iter); } /* VETH case: NetworkManager doesn't enforce the existence of the veth peer. * NM will create one connection for each veth in the pair. In this case, due to our integration with * NM (netplan-everywhere), we can't enforce the existence of both peers at a given moment because * they might be created one after the other. * When we find that the peer is missing, we create a temporary one using the placeholder type. That is necessary * so we can generate the keyfile referring to the correct peer name, even though it still doesn't exist. */ if (netdef->type == NETPLAN_DEF_TYPE_VETH && backend == NETPLAN_BACKEND_NM) { netdef->veth_peer_link = netplan_netdef_new(npp, scalar(missing->node), NETPLAN_DEF_TYPE_NM_PLACEHOLDER_, NETPLAN_BACKEND_NM); g_hash_table_iter_remove(&iter); } } } /** * Handle multiple-pass parsing of the yaml document. */ STATIC gboolean process_document(NetplanParser* npp, GError** error) { gboolean ret; int previously_found; int still_missing; g_assert(npp->missing_id == NULL); npp->missing_id = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); do { g_debug("starting new processing pass"); previously_found = npp->missing_ids_found; npp->missing_ids_found = 0; g_clear_error(error); ret = process_mapping(npp, yaml_document_get_root_node(&npp->doc), "", root_handlers, NULL, error); still_missing = g_hash_table_size(npp->missing_id); if (still_missing > 0 && npp->missing_ids_found == previously_found) break; } while (still_missing > 0 || npp->missing_ids_found > 0); /* If an error already occurred we should return and not assume it's a missing interface*/ if (error && *error) goto cleanup; process_missing_ids(npp, error); if (g_hash_table_size(npp->missing_id) > 0) { GHashTableIter iter; gpointer key, value; NetplanMissingNode *missing; g_clear_error(error); /* Get the first missing identifier we can get from our list, to * approximate early failure and give the user a meaningful error. */ g_hash_table_iter_init (&iter, npp->missing_id); g_hash_table_iter_next (&iter, &key, &value); missing = (NetplanMissingNode*) value; ret = yaml_error(npp, missing->node, error, "%s: interface '%s' is not defined", missing->netdef_id, (char*)key); goto cleanup; } cleanup: g_hash_table_destroy(npp->missing_id); npp->missing_id = NULL; return ret; } STATIC gboolean _netplan_parser_load_single_file(NetplanParser* npp, const char *opt_filepath, yaml_document_t *doc, GError** error) { int ret = FALSE; if (opt_filepath) { char* source = g_strdup(opt_filepath); if (!npp->sources) npp->sources = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); g_hash_table_add(npp->sources, source); } /* empty file? */ if (yaml_document_get_root_node(doc) == NULL) return TRUE; g_assert(npp->ids_in_file == NULL); npp->ids_in_file = g_hash_table_new(g_str_hash, NULL); npp->current.filepath = opt_filepath? g_strdup(opt_filepath) : NULL; ret = process_document(npp, error); g_free((void *)npp->current.filepath); npp->current.filepath = NULL; yaml_document_delete(doc); g_hash_table_destroy(npp->ids_in_file); npp->ids_in_file = NULL; return ret; } gboolean netplan_parser_load_yaml_from_fd(NetplanParser* npp, int fd, GError** error) { yaml_document_t *doc = &npp->doc; if (!load_yaml_from_fd(fd, doc, error)) return FALSE; return _netplan_parser_load_single_file(npp, NULL, doc, error); } gboolean netplan_parser_load_yaml(NetplanParser* npp, const char* filename, GError** error) { yaml_document_t *doc = &npp->doc; /* Log a warning if a file can be read or written by a non-owner. * It could contain sensitive information (e.g. WiFi passwords), so should * stay secret. */ mode_t mask = S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; struct stat info; if (stat(filename, &info) < 0) { g_set_error(error, NETPLAN_FILE_ERROR, errno, "Cannot stat %s: %m", filename); return FALSE; } else if (info.st_mode & mask) g_warning("Permissions for %s are too open. Netplan configuration " "should NOT be accessible by others.", filename); if (!load_yaml(filename, doc, error)) return FALSE; return _netplan_parser_load_single_file(npp, filename, doc, error); } STATIC gboolean finish_iterator(const NetplanParser* npp, NetplanNetDefinition* nd, GError **error) { /* Take more steps to make sure we always have a backend set for netdefs */ if (nd->backend == NETPLAN_BACKEND_NONE) { nd->backend = get_default_backend_for_type(npp->global_backend, nd->type); g_debug("%s: setting default backend to %i", nd->id, nd->backend); } /* Do a final pass of validation for backend-specific conditions */ return validate_backend_rules(npp, nd, error) && validate_sriov_rules(npp, nd, error); } STATIC gboolean insert_kv_into_hash(void *key, void *value, void *hash) { g_hash_table_insert(hash, key, value); return TRUE; } gboolean netplan_state_import_parser_results(NetplanState* np_state, NetplanParser* npp, GError** error) { if (npp->parsed_defs) { GError *recoverable = NULL; GHashTableIter iter; gpointer key, value; char *regdom = NULL; g_debug("We have some netdefs, pass them through a final round of validation"); /* Check/adopt VRF routes before route consistency and validation */ if (!adopt_and_validate_vrf_routes(npp, npp->parsed_defs, error)) return FALSE; if (!validate_default_route_consistency(npp, npp->parsed_defs, &recoverable)) { g_warning("Problem encountered while validating default route consistency." "Please set up multiple routing tables and use `routing-policy` instead.\n" "Error: %s", (recoverable) ? recoverable->message : ""); g_clear_error(&recoverable); } g_hash_table_iter_init (&iter, npp->parsed_defs); while (g_hash_table_iter_next (&iter, &key, &value)) { g_assert(np_state->netdefs == NULL || g_hash_table_lookup(np_state->netdefs, key) == NULL); NetplanNetDefinition *nd = value; if (nd->regulatory_domain) { if (!regdom) regdom = nd->regulatory_domain; else if (g_strcmp0(regdom, nd->regulatory_domain) != 0) g_warning("%s: Conflicting regulatory-domain (%s vs %s)", nd->id, regdom, nd->regulatory_domain); } if (!finish_iterator(npp, nd, error)) return FALSE; g_debug("Configuration is valid"); } } if (npp->parsed_defs) { if (!np_state->netdefs) np_state->netdefs = g_hash_table_new(g_str_hash, g_str_equal); g_hash_table_foreach_steal(npp->parsed_defs, insert_kv_into_hash, np_state->netdefs); } np_state->netdefs_ordered = g_list_concat(np_state->netdefs_ordered, npp->ordered); np_state->ovs_settings = npp->global_ovs_settings; np_state->backend = npp->global_backend; if (npp->sources) { if (!np_state->sources) np_state->sources = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); g_hash_table_foreach_steal(npp->sources, insert_kv_into_hash, np_state->sources); } if (npp->global_renderer) { if (!np_state->global_renderer) np_state->global_renderer = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); g_hash_table_foreach_steal(npp->global_renderer, insert_kv_into_hash, np_state->global_renderer); } /* We need to reset those fields manually as we transfered ownership of the underlying data to out. If we don't do this, netplan_clear_parser will deallocate data that we don't own anymore. */ npp->ordered = NULL; memset(&npp->global_ovs_settings, 0, sizeof(NetplanOVSSettings)); netplan_parser_reset(npp); return TRUE; } NetplanParser* netplan_parser_new() { NetplanParser* npp = g_new0(NetplanParser, 1); netplan_parser_reset(npp); return npp; } void netplan_parser_reset(NetplanParser* npp) { g_assert(npp != NULL); if(npp->parsed_defs) { /* FIXME: make sure that any dynamically allocated netdef data is freed */ g_hash_table_destroy(npp->parsed_defs); npp->parsed_defs = NULL; } if(npp->ordered) { g_clear_list(&npp->ordered, clear_netdef_from_list); npp->ordered = NULL; } npp->global_backend = NETPLAN_BACKEND_NONE; reset_ovs_settings(&npp->global_ovs_settings); /* These pointers are non-owning, it's not our place to free their resources*/ npp->current.netdef = NULL; npp->current.auth = NULL; npp->current.vxlan = NULL; access_point_clear(&npp->current.access_point, npp->current.backend); wireguard_peer_clear(&npp->current.wireguard_peer); address_options_clear(&npp->current.addr_options); route_clear(&npp->current.route); ip_rule_clear(&npp->current.ip_rule); g_free((void *)npp->current.filepath); npp->current.filepath = NULL; // LCOV_EXCL_START if (npp->ids_in_file) { g_hash_table_destroy(npp->ids_in_file); npp->ids_in_file = NULL; } // LCOV_EXCL_STOP if (npp->missing_id) { g_hash_table_destroy(npp->missing_id); npp->missing_id = NULL; } npp->missing_ids_found = 0; if (npp->null_fields) { g_hash_table_destroy(npp->null_fields); npp->null_fields = NULL; } if (npp->null_overrides) { g_hash_table_destroy(npp->null_overrides); npp->null_overrides = NULL; } if (npp->sources) { /* Properly configured at creation not to leak */ g_hash_table_destroy(npp->sources); npp->sources = NULL; } if (npp->global_renderer) { g_hash_table_destroy(npp->global_renderer); npp->global_renderer = NULL; } } void netplan_parser_clear(NetplanParser** npp_p) { NetplanParser* npp = *npp_p; *npp_p = NULL; netplan_parser_reset(npp); g_free(npp); } /* Check if this is a Netdef-ID or global keyword which can be nullified. * Overrides (depending on YAML hierarchy) can only happen on global values * (like "renderer") or on the individual netdef level. * @return the Netdef-ID/keyword or NULL */ STATIC gboolean is_netdef_id_or_global_value(const char* full_key) { g_autofree gchar* key = g_strstrip(g_strdup(full_key)); // strip leading '\t' gboolean ret = FALSE; gchar** split = g_strsplit(key, "\t", 0); if (split[0] && g_strcmp0(split[0], "network") == 0) { if (split[1]) { if (g_strcmp0(split[1], "renderer") == 0) { ret = TRUE; // a valid global keyword goto cleanup; } /* check if is valid network type */ for (unsigned i = 0; i < NETPLAN_DEF_TYPE_MAX_; ++i) { const char* def_type_name = netplan_def_type_name(i); if (def_type_name && g_strcmp0(split[1], def_type_name) == 0) { /* return keyword if split[2] is a Netdef-ID * e.g. "network.ethernets.eth0" */ if (split[2] && !split[3]) { ret = TRUE; // a valid Netdef-ID break; } } } } } cleanup: g_strfreev(split); return ret; } STATIC void extract_null_fields(yaml_document_t* doc, yaml_node_t* node, GHashTable* null_fields, char* key_prefix, const char* origin_hint) { yaml_node_pair_t* entry; switch (node->type) { // LCOV_EXCL_START case YAML_NO_NODE: g_hash_table_insert(null_fields, key_prefix, NULL); key_prefix = NULL; break; // LCOV_EXCL_STOP case YAML_SCALAR_NODE: if ( g_ascii_strcasecmp("null", scalar(node)) == 0 || g_strcmp0((char*)node->tag, YAML_NULL_TAG) == 0 || g_strcmp0(scalar(node), "~") == 0) { g_hash_table_insert(null_fields, key_prefix, NULL); key_prefix = NULL; } break; case YAML_SEQUENCE_NODE: /* Do nothing, we don't support nullifying *inside* sequences */ break; case YAML_MAPPING_NODE: for (entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { yaml_node_t* key, *value; char* full_key; key = yaml_document_get_node(doc, entry->key); value = yaml_document_get_node(doc, entry->value); full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value); /* If an origin_hint is given, nullify the overrides, like * Netdef-IDs or global values (e.g. "renderer") and track the * origin_hint filename as hashmap value. To ignore such netdefs * or globals during the YAML parsing stage should they be * defined somewhere else outside the origin-hint file. */ if (origin_hint && is_netdef_id_or_global_value(full_key)) { g_hash_table_insert(null_fields, g_strdup(full_key), g_strdup(origin_hint)); g_debug("ignoring previous definition of: %s (except in %s)", full_key, origin_hint); } extract_null_fields(doc, value, null_fields, full_key, origin_hint); } break; // LCOV_EXCL_START default: g_assert(FALSE); // supposedly unreachable! // LCOV_EXCL_STOP } g_free(key_prefix); } gboolean netplan_parser_load_nullable_fields(NetplanParser* npp, int input_fd, GError** error) { yaml_document_t doc; if (!load_yaml_from_fd(input_fd, &doc, error)) return FALSE; // LCOV_EXCL_LINE /* empty file? */ if (yaml_document_get_root_node(&doc) == NULL) return TRUE; // LCOV_EXCL_LINE if (!npp->null_fields) npp->null_fields = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); extract_null_fields(&doc, yaml_document_get_root_node(&doc), npp->null_fields, g_strdup(""), NULL); yaml_document_delete(&doc); return TRUE; } gboolean netplan_parser_load_nullable_overrides( NetplanParser* npp, int input_fd, const char* constraint, GError** error) { yaml_document_t doc; if (!load_yaml_from_fd(input_fd, &doc, error)) return FALSE; // LCOV_EXCL_LINE /* empty file? */ if (yaml_document_get_root_node(&doc) == NULL) return TRUE; // LCOV_EXCL_LINE if (!npp->null_overrides) npp->null_overrides = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); /* Track the given origin_hint filename, as a constraint, for any netdef or * global value of the given (i.e. YAML patch), so that those can * be ignored later (inside YAML the parsing stage), shouldn't they * originate from the origin-hint file, but from some other YAML file inside * the hierarchy. * * Examples for "origin_hint:hint.yaml" being tracked in npp->null_overrides: * yaml patch: "network.ethernets.eth0.dhcp4=false" * => network.ethernets.eth0: hint.yaml * yaml patch: "network.renderer=NetworkManager" * => network.renderer: hint.yaml */ extract_null_fields(&doc, yaml_document_get_root_node(&doc), npp->null_overrides, g_strdup(""), constraint); yaml_document_delete(&doc); return TRUE; } netplan-1.0/src/sriov.c000066400000000000000000000200101457004145200151030ustar00rootroot00000000000000/* * Copyright (C) 2020-2022 Canonical, Ltd. * Author: Łukasz 'sil2100' Zemczak * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include "util-internal.h" #include "sriov.h" STATIC gboolean write_sriov_rebind_systemd_unit(GHashTable* pfs, const char* rootdir, GError** error) { g_autofree gchar* id_escaped = NULL; g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/multi-user.target.wants/netplan-sriov-rebind.service", NULL); g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-sriov-rebind.service", NULL); GHashTableIter iter; gpointer key; GString* interfaces = g_string_new(""); GString* s = g_string_new("[Unit]\n"); g_string_append(s, "Description=(Re-)bind SR-IOV Virtual Functions to their driver\n"); g_string_append_printf(s, "After=network.target\n"); g_string_append_printf(s, "After=netplan-sriov-apply.service\n"); /* Run after udev */ g_hash_table_iter_init(&iter, pfs); while (g_hash_table_iter_next (&iter, &key, NULL)) { const gchar* id = key; g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", id); g_string_append_printf(interfaces, "%s ", id); } g_string_append(s, "\n[Service]\nType=oneshot\n"); g_string_truncate(interfaces, interfaces->len-1); /* cut trailing whitespace */ g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind --debug %s\n", interfaces->str); _netplan_g_string_free_to_file(s, rootdir, path, NULL); g_string_free(interfaces, TRUE); _netplan_safe_mkdir_p_dir(link); if (symlink(path, link) < 0 && errno != EEXIST) { // LCOV_EXCL_START g_set_error(error, NETPLAN_FILE_ERROR, errno, "failed to create enablement symlink: %m"); return FALSE; // LCOV_EXCL_STOP } return TRUE; } STATIC gboolean write_sriov_apply_systemd_unit(GHashTable* pfs, const char* rootdir, GError** error) { g_autofree gchar* id_escaped = NULL; g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/multi-user.target.wants/netplan-sriov-apply.service", NULL); g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-sriov-apply.service", NULL); GHashTableIter iter; gpointer key; GString* s = g_string_new("[Unit]\n"); g_string_append(s, "Description=Apply SR-IOV configuration\n"); g_string_append(s, "DefaultDependencies=no\n"); g_string_append(s, "Before=network-pre.target\n"); g_hash_table_iter_init(&iter, pfs); while (g_hash_table_iter_next (&iter, &key, NULL)) { g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", (gchar*) key); } g_string_append(s, "\n[Service]\nType=oneshot\n"); g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan apply --sriov-only\n"); _netplan_g_string_free_to_file(s, rootdir, path, NULL); _netplan_safe_mkdir_p_dir(link); if (symlink(path, link) < 0 && errno != EEXIST) { // LCOV_EXCL_START g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, "failed to create enablement symlink: %m"); return FALSE; // LCOV_EXCL_STOP } return TRUE; } /** * Finalize the SR-IOV configuration (global config) */ gboolean netplan_state_finish_sriov_write(const NetplanState* np_state, const char* rootdir, __unused GError** error) { NetplanNetDefinition* def = NULL; NetplanNetDefinition* pf = NULL; gboolean any_sriov = FALSE; gboolean ret = TRUE; if (np_state) { GHashTable* rebind_pfs = g_hash_table_new(g_str_hash, g_str_equal); GHashTable* apply_pfs = g_hash_table_new(g_str_hash, g_str_equal); /* Find netdev interface names for SR-IOV PFs*/ for (GList* iterator = np_state->netdefs_ordered; iterator; iterator = iterator->next) { def = (NetplanNetDefinition*) iterator->data; pf = NULL; if (def->sriov_explicit_vf_count < G_MAXUINT || def->sriov_link) { any_sriov = TRUE; if (def->sriov_explicit_vf_count < G_MAXUINT) pf = def; else if (def->sriov_link) pf = def->sriov_link; if (pf) { if (pf->set_name) g_hash_table_add(apply_pfs, pf->set_name); else if (!pf->has_match) /* netdef_id == interface name */ g_hash_table_add(apply_pfs, pf->id); else g_warning("%s: Cannot determine SR-IOV PF interface name.", pf->id); } } if (pf && pf->sriov_delay_virtual_functions_rebind) { if (pf->set_name) g_hash_table_add(rebind_pfs, pf->set_name); else if (!pf->has_match) /* netdef_id == interface name */ g_hash_table_add(rebind_pfs, pf->id); else g_warning("%s: Cannot rebind SR-IOV virtual functions, unknown interface name. " "Use 'netplan rebind ' to rebind manually or use the 'set-name' stanza.", pf->id); } } if (any_sriov) { ret = write_sriov_apply_systemd_unit(apply_pfs, rootdir, NULL); if (!ret) { // LCOV_EXCL_START g_warning("netplan-sriov-apply.service cannot be created."); goto error; // LCOV_EXCL_STOP } /* * The sriov-apply service will always be created (as long as there is any sr-iov configuration) * and the sriov-rebind MUST only run after apply. As sriov-apply will always be there if sriov-rebind * is present, using the After= dependency statement is enough (Requires= is not necessary). */ if (g_hash_table_size(rebind_pfs) > 0) { ret = write_sriov_rebind_systemd_unit(rebind_pfs, rootdir, NULL); if (!ret) // LCOV_EXCL_START g_warning("netplan-sriov-rebind.service cannot be created."); // LCOV_EXCL_STOP } } error: g_hash_table_destroy(rebind_pfs); g_hash_table_destroy(apply_pfs); } return ret; } gboolean _netplan_sriov_cleanup(const char* rootdir) { _netplan_unlink_glob(rootdir, "/run/udev/rules.d/*-sriov-netplan-*.rules"); _netplan_unlink_glob(rootdir, "/run/systemd/system/netplan-sriov-*.service"); return TRUE; } int _netplan_state_get_vf_count_for_def(const NetplanState* np_state, const NetplanNetDefinition* netdef, GError** error) { GHashTableIter iter; gpointer key, value; guint count = 0; g_hash_table_iter_init(&iter, np_state->netdefs); while (g_hash_table_iter_next (&iter, &key, &value)) { const NetplanNetDefinition* def = value; if (def->sriov_link == netdef) count++; } if (netdef->sriov_explicit_vf_count != G_MAXUINT && count > netdef->sriov_explicit_vf_count) { g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_VALIDATION, "more VFs allocated than the explicit size declared: %d > %d", count, netdef->sriov_explicit_vf_count); return -1; } return netdef->sriov_explicit_vf_count != G_MAXUINT ? netdef->sriov_explicit_vf_count : count; } netplan-1.0/src/sriov.h000066400000000000000000000014451457004145200151230ustar00rootroot00000000000000/* * Copyright (C) 2020 Canonical, Ltd. * Author: Łukasz 'sil2100' Zemczak * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "netplan.h" NETPLAN_INTERNAL gboolean _netplan_sriov_cleanup(const char* rootdir); netplan-1.0/src/types-internal.h000066400000000000000000000206651457004145200167440ustar00rootroot00000000000000/* * Copyright (C) 2021 Canonical, Ltd. * Author: Simon Chopin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "parse.h" #include #include #include /* Quite a few types are part of our current ABI, and so were isolated * in order to make it easier to tell what's fair game and allow for ABI * compatibility checks using 'abidiff' (abigail-tools). */ #include "abi.h" typedef enum { NETPLAN_ADDRGEN_DEFAULT, NETPLAN_ADDRGEN_EUI64, NETPLAN_ADDRGEN_STABLEPRIVACY, NETPLAN_ADDRGEN_MAX, } NetplanAddrGenMode; struct NetplanOptionalAddressType { char* name; NetplanOptionalAddressFlag flag; }; typedef enum { NETPLAN_VXLAN_NOTIFICATION_L2_MISS = 1<<0, NETPLAN_VXLAN_NOTIFICATION_L3_MISS = 1<<1, } NetplanVxlanNotificationFlags; typedef enum { NETPLAN_VXLAN_CHECKSUM_UDP = 1<<0, NETPLAN_VXLAN_CHECKSUM_ZERO_UDP6_TX = 1<<1, NETPLAN_VXLAN_CHECKSUM_ZERO_UDP6_RX = 1<<2, NETPLAN_VXLAN_CHECKSUM_REMOTE_TX = 1<<3, NETPLAN_VXLAN_CHECKSUM_REMOTE_RX = 1<<4, } NetplanVxlanChecksumFlags; typedef enum { NETPLAN_VXLAN_EXTENSION_GROUP_POLICY = 1<<0, NETPLAN_VXLAN_EXTENSION_GENERIC_PROTOCOL = 1<<1, } NetplanVxlanExtensionFlags; // Not strictly speaking a type, but seems fair to keep it around. extern struct NetplanOptionalAddressType NETPLAN_OPTIONAL_ADDRESS_TYPES[]; extern struct NetplanWifiWowlanType NETPLAN_WIFI_WOWLAN_TYPES[]; typedef struct missing_node { char* netdef_id; const yaml_node_t* node; } NetplanMissingNode; struct private_netdef_data { GHashTable* dirty_fields; }; typedef enum { NETPLAN_WIFI_MODE_INFRASTRUCTURE, NETPLAN_WIFI_MODE_ADHOC, NETPLAN_WIFI_MODE_AP, NETPLAN_WIFI_MODE_OTHER, NETPLAN_WIFI_MODE_MAX_ } NetplanWifiMode; typedef struct { char *endpoint; char *public_key; char *preshared_key; GArray *allowed_ips; guint keepalive; } NetplanWireguardPeer; typedef enum { NETPLAN_WIFI_BAND_DEFAULT, NETPLAN_WIFI_BAND_5, NETPLAN_WIFI_BAND_24 } NetplanWifiBand; typedef struct { char* address; char* lifetime; char* label; } NetplanAddressOptions; struct address_iter { guint ip4_index; guint ip6_index; guint address_options_index; NetplanNetDefinition* netdef; NetplanAddressOptions* last_address; }; struct nameserver_iter { guint ip4_index; guint ip6_index; guint search_index; NetplanNetDefinition* netdef; }; struct route_iter { guint route_index; NetplanNetDefinition* netdef; }; typedef struct { NetplanWifiMode mode; char* ssid; NetplanWifiBand band; char* bssid; gboolean hidden; guint channel; NetplanAuthenticationSettings auth; gboolean has_auth; NetplanBackendSettings backend_settings; } NetplanWifiAccessPoint; typedef struct { gint family; char* type; char* scope; guint table; char* from; char* to; char* via; gboolean onlink; /* valid metrics are valid positive integers. * invalid metrics are represented by METRIC_UNSPEC */ guint metric; guint mtubytes; guint congestion_window; guint advertised_receive_window; } NetplanIPRoute; typedef struct { gint family; char* from; char* to; /* table: Valid values are 1 <= x <= 4294967295) */ guint table; guint priority; /* fwmark: Valid values are 1 <= x <= 4294967295) */ guint fwmark; /* type-of-service: between 0 and 255 */ guint tos; } NetplanIPRule; struct netplan_vxlan { NetplanNetDefinition* link; guint vni; guint ageing; guint limit; guint tos; guint flow_label; guint source_port_min; guint source_port_max; NetplanTristate mac_learning; NetplanTristate arp_proxy; NetplanTristate short_circuit; gboolean independent; NetplanFlags notifications; NetplanFlags checksums; NetplanFlags extensions; NetplanTristate do_not_fragment; }; struct netplan_state { /* Since both netdefs and netdefs_ordered store pointers to the same elements, * we consider that only netdefs_ordered is owner of this data. One should not * free() objects obtained from netdefs, and proper care should be taken to remove * any reference of an object in netdefs when destroying it from netdefs_ordered. */ GHashTable *netdefs; GList *netdefs_ordered; NetplanBackend backend; NetplanOVSSettings ovs_settings; /* Hashset of the source files used to create this state. Owns its data (glib-allocated * char*) and is initialized with g_hash_table_new_full to avoid leaks. */ GHashTable* sources; GHashTable* global_renderer; }; struct netplan_parser { yaml_document_t doc; /* Netplan definitions that have already been processed. * Weak references to the nedefs */ GHashTable* parsed_defs; /* Same definitions, stored in the order of processing. * Owning structure for the netdefs */ GList* ordered; NetplanBackend global_backend; NetplanOVSSettings global_ovs_settings; /* Keep track of the files used as data source */ GHashTable* sources; /* Data currently being processed */ struct { /* Refs to objects allocated elsewhere */ NetplanNetDefinition* netdef; NetplanAuthenticationSettings *auth; /* Owned refs, not yet referenced anywhere */ NetplanWifiAccessPoint *access_point; NetplanWireguardPeer* wireguard_peer; NetplanAddressOptions* addr_options; NetplanIPRoute* route; NetplanIPRule* ip_rule; NetplanVxlan* vxlan; const char *filepath; /* Plain old data representing the backend for which we are * currently parsing. Not necessarily the same as the global * backend. */ NetplanBackend backend; } current; /* List of "seen" ids not found in netdefs yet by the parser. * These are removed when it exists in this list and we reach the point of * creating a netdef for that id; so by the time we're done parsing the yaml * document it should be empty. * * Keys are not owned, but the values are. Should be created with NULL and g_free * destructors, respectively, so that the cleanup is automatic at destruction. */ GHashTable* missing_id; /* Set of IDs in currently parsed YAML file, for being able to detect * "duplicate ID within one file" vs. allowing a drop-in to override/amend an * existing definition. * * Appears to be unused? * */ GHashTable* ids_in_file; int missing_ids_found; /* Which fields have been nullified by a subsequent patch? */ GHashTable* null_fields; GHashTable* null_overrides; GHashTable* global_renderer; }; struct netplan_state_iterator { GList* next; }; #define NETPLAN_ADVERTISED_RECEIVE_WINDOW_UNSPEC 0 #define NETPLAN_CONGESTION_WINDOW_UNSPEC 0 #define NETPLAN_MTU_UNSPEC 0 #define NETPLAN_METRIC_UNSPEC G_MAXUINT #define NETPLAN_ROUTE_TABLE_UNSPEC 0 #define NETPLAN_IP_RULE_PRIO_UNSPEC G_MAXUINT #define NETPLAN_IP_RULE_FW_MARK_UNSPEC 0 #define NETPLAN_IP_RULE_TOS_UNSPEC G_MAXUINT #if defined(UNITTESTS) #define STATIC #else #define STATIC static #endif void reset_netdef(NetplanNetDefinition* netdef, NetplanDefType type, NetplanBackend renderer); void reset_ip_rule(NetplanIPRule* ip_rule); void reset_ovs_settings(NetplanOVSSettings *settings); void reset_vxlan(NetplanVxlan* vxlan); void access_point_clear(NetplanWifiAccessPoint** ap, NetplanBackend backend); void wireguard_peer_clear(NetplanWireguardPeer** peer); void address_options_clear(NetplanAddressOptions** options); void ip_rule_clear(NetplanIPRule** rule); void route_clear(NetplanIPRoute** route); gboolean netplan_state_has_nondefault_globals(const NetplanState* np_state); void clear_netdef_from_list(void* def); void free_address_options(void* ptr); void free_access_point(void* key, void* value, void* data); netplan-1.0/src/types.c000066400000000000000000000453451457004145200151270ustar00rootroot00000000000000/* * Copyright (C) 2021 Canonical, Ltd. * Author: Simon Chopin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* This module contains functions to deal with the Netplan objects, * notably, accessors and destructors. Note that types specific to parsing * are implemented separately. */ #include #include "types-internal.h" #include "util-internal.h" #define FREE_AND_NULLIFY(ptr) { g_free(ptr); ptr = NULL; } /* Helper function to free a GArray after applying a destructor to its * elements. Note that in the most trivial case (g_free) we should probably * have used a GPtrArray directly... */ STATIC void free_garray_with_destructor(GArray** array, void (destructor)(void *)) { if (*array) { for (size_t i = 0; i < (*array)->len; ++i) { void* ptr = g_array_index(*array, char*, i); destructor(ptr); } g_array_free(*array, TRUE); *array = NULL; } } /* Helper function to free a GHashTable after applying a simple destructor to its * elements. */ STATIC void free_hashtable_with_destructor(GHashTable** hash, void (destructor)(void *)) { if (*hash) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, *hash); while (g_hash_table_iter_next(&iter, &key, &value)) { destructor(key); destructor(value); } g_hash_table_destroy(*hash); *hash = NULL; } } void free_address_options(void* ptr) { NetplanAddressOptions* opts = ptr; g_free(opts->address); g_free(opts->label); g_free(opts->lifetime); g_free(opts); } STATIC void free_route(void* ptr) { NetplanIPRoute* route = ptr; g_free(route->scope); g_free(route->type); g_free(route->to); g_free(route->from); g_free(route->via); g_free(route); } STATIC void free_ip_rules(void* ptr) { NetplanIPRule* rule = ptr; g_free(rule->to); g_free(rule->from); g_free(rule); } STATIC void free_wireguard_peer(void* ptr) { NetplanWireguardPeer* wg = ptr; g_free(wg->endpoint); g_free(wg->preshared_key); g_free(wg->public_key); free_garray_with_destructor(&wg->allowed_ips, g_free); g_free(wg); } STATIC void reset_auth_settings(NetplanAuthenticationSettings* auth) { FREE_AND_NULLIFY(auth->identity); FREE_AND_NULLIFY(auth->anonymous_identity); FREE_AND_NULLIFY(auth->password); FREE_AND_NULLIFY(auth->psk); FREE_AND_NULLIFY(auth->ca_certificate); FREE_AND_NULLIFY(auth->client_certificate); FREE_AND_NULLIFY(auth->client_key); FREE_AND_NULLIFY(auth->client_key_password); FREE_AND_NULLIFY(auth->phase2_auth); auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_NONE; auth->eap_method = NETPLAN_AUTH_EAP_NONE; auth->pmf_mode = NETPLAN_AUTH_PMF_MODE_NONE; } void reset_ovs_settings(NetplanOVSSettings* settings) { settings->mcast_snooping = FALSE; settings->rstp = FALSE; free_hashtable_with_destructor(&settings->external_ids, g_free); free_hashtable_with_destructor(&settings->other_config, g_free); FREE_AND_NULLIFY(settings->lacp); FREE_AND_NULLIFY(settings->fail_mode); free_garray_with_destructor(&settings->protocols, g_free); reset_auth_settings(&settings->ssl); free_garray_with_destructor(&settings->controller.addresses, g_free); FREE_AND_NULLIFY(settings->controller.connection_mode); } STATIC void reset_dhcp_overrides(NetplanDHCPOverrides* overrides) { overrides->use_dns = TRUE; FREE_AND_NULLIFY(overrides->use_domains); overrides->use_ntp = TRUE; overrides->send_hostname = TRUE; overrides->use_hostname = TRUE; overrides->use_mtu = TRUE; overrides->use_routes = TRUE; FREE_AND_NULLIFY(overrides->hostname); overrides->metric = NETPLAN_METRIC_UNSPEC; } void reset_ip_rule(NetplanIPRule* ip_rule) { ip_rule->family = -1; /* 0 is a valid family ID */ ip_rule->priority = NETPLAN_IP_RULE_PRIO_UNSPEC; ip_rule->table = NETPLAN_ROUTE_TABLE_UNSPEC; ip_rule->tos = NETPLAN_IP_RULE_TOS_UNSPEC; ip_rule->fwmark = NETPLAN_IP_RULE_FW_MARK_UNSPEC; } /* Reset a backend settings object. */ STATIC void reset_backend_settings(NetplanBackendSettings* settings) { FREE_AND_NULLIFY(settings->name); FREE_AND_NULLIFY(settings->uuid); FREE_AND_NULLIFY(settings->stable_id); FREE_AND_NULLIFY(settings->device); g_datalist_clear(&settings->passthrough); } STATIC void reset_private_netdef_data(struct private_netdef_data* data) { if (!data) return; if (data->dirty_fields) g_hash_table_destroy(data->dirty_fields); data->dirty_fields = NULL; } void reset_vxlan(NetplanVxlan* vxlan) { if (!vxlan) return; memset(vxlan, 0, sizeof(NetplanVxlan)); vxlan->link = NULL; vxlan->flow_label = G_MAXUINT; vxlan->do_not_fragment = NETPLAN_TRISTATE_UNSET; vxlan->mac_learning = NETPLAN_TRISTATE_UNSET; vxlan->arp_proxy = NETPLAN_TRISTATE_UNSET; vxlan->short_circuit = NETPLAN_TRISTATE_UNSET; } /* Free a heap-allocated NetplanWifiAccessPoint object. * Signature made to match the g_hash_table_foreach function. * @key: ignored * @value: pointer to a heap-allocated NetlpanWifiAccessPoint object * @data: pointer to a NetplanBackend value representing the renderer context in which * to interpret the processed object, especially regarding the backend settings */ void free_access_point(__unused void* key, void* value, __unused void* data) { NetplanWifiAccessPoint* ap = value; g_free(ap->ssid); g_free(ap->bssid); reset_auth_settings(&ap->auth); reset_backend_settings(&ap->backend_settings); g_free(ap); } /* Reset a given network definition to its initial state, releasing any owned data */ void reset_netdef(NetplanNetDefinition* netdef, NetplanDefType new_type, NetplanBackend new_backend) { /* Needed for some cleanups down the line */ NetplanBackend backend = netdef->backend; netdef->type = new_type; netdef->backend = new_backend; FREE_AND_NULLIFY(netdef->id); memset(netdef->uuid, 0, sizeof(netdef->uuid)); netdef->optional = FALSE; netdef->optional_addresses = 0; netdef->critical = FALSE; netdef->dhcp4 = FALSE; netdef->dhcp6 = FALSE; FREE_AND_NULLIFY(netdef->dhcp_identifier); reset_dhcp_overrides(&netdef->dhcp4_overrides); reset_dhcp_overrides(&netdef->dhcp6_overrides); netdef->accept_ra = NETPLAN_RA_MODE_KERNEL; free_garray_with_destructor(&netdef->ip4_addresses, g_free); free_garray_with_destructor(&netdef->ip6_addresses, g_free); free_garray_with_destructor(&netdef->address_options, free_address_options); netdef->ip6_privacy = FALSE; netdef->ip6_addr_gen_mode = NETPLAN_ADDRGEN_DEFAULT; FREE_AND_NULLIFY(netdef->ip6_addr_gen_token); FREE_AND_NULLIFY(netdef->gateway4); FREE_AND_NULLIFY(netdef->gateway6); FREE_AND_NULLIFY(netdef->regulatory_domain); free_garray_with_destructor(&netdef->ip4_nameservers, g_free); free_garray_with_destructor(&netdef->ip6_nameservers, g_free); free_garray_with_destructor(&netdef->search_domains, g_free); free_garray_with_destructor(&netdef->routes, free_route); free_garray_with_destructor(&netdef->ip_rules, free_ip_rules); free_garray_with_destructor(&netdef->wireguard_peers, free_wireguard_peer); netdef->linklocal.ipv4 = FALSE; netdef->linklocal.ipv6 = TRUE; FREE_AND_NULLIFY(netdef->bridge); FREE_AND_NULLIFY(netdef->bond); FREE_AND_NULLIFY(netdef->peer); netdef->bridge_link = NULL; netdef->bond_link = NULL; netdef->peer_link = NULL; netdef->vlan_id = G_MAXUINT; /* 0 is a valid ID */ netdef->vlan_link = NULL; netdef->has_vlans = FALSE; netdef->vrf_link = NULL; netdef->vrf_table = G_MAXUINT; FREE_AND_NULLIFY(netdef->set_mac); netdef->mtubytes = 0; netdef->ipv6_mtubytes = 0; FREE_AND_NULLIFY(netdef->set_name); FREE_AND_NULLIFY(netdef->match.driver); FREE_AND_NULLIFY(netdef->match.mac); FREE_AND_NULLIFY(netdef->match.original_name); netdef->has_match = FALSE; netdef->wake_on_lan = FALSE; netdef->wowlan = 0; netdef->emit_lldp = FALSE; if (netdef->access_points) { g_hash_table_foreach(netdef->access_points, free_access_point, &backend); g_hash_table_destroy(netdef->access_points); netdef->access_points = NULL; } FREE_AND_NULLIFY(netdef->bond_params.mode); FREE_AND_NULLIFY(netdef->bond_params.lacp_rate); FREE_AND_NULLIFY(netdef->bond_params.monitor_interval); FREE_AND_NULLIFY(netdef->bond_params.transmit_hash_policy); FREE_AND_NULLIFY(netdef->bond_params.selection_logic); FREE_AND_NULLIFY(netdef->bond_params.arp_interval); free_garray_with_destructor(&netdef->bond_params.arp_ip_targets, g_free); FREE_AND_NULLIFY(netdef->bond_params.arp_validate); FREE_AND_NULLIFY(netdef->bond_params.arp_all_targets); FREE_AND_NULLIFY(netdef->bond_params.up_delay); FREE_AND_NULLIFY(netdef->bond_params.down_delay); FREE_AND_NULLIFY(netdef->bond_params.fail_over_mac_policy); FREE_AND_NULLIFY(netdef->bond_params.primary_reselect_policy); FREE_AND_NULLIFY(netdef->bond_params.learn_interval); FREE_AND_NULLIFY(netdef->bond_params.primary_member); memset(&netdef->bond_params, 0, sizeof(netdef->bond_params)); netdef->has_vxlans = FALSE; reset_vxlan(netdef->vxlan); FREE_AND_NULLIFY(netdef->vxlan); FREE_AND_NULLIFY(netdef->modem_params.apn); FREE_AND_NULLIFY(netdef->modem_params.device_id); FREE_AND_NULLIFY(netdef->modem_params.network_id); FREE_AND_NULLIFY(netdef->modem_params.number); FREE_AND_NULLIFY(netdef->modem_params.password); FREE_AND_NULLIFY(netdef->modem_params.pin); FREE_AND_NULLIFY(netdef->modem_params.sim_id); FREE_AND_NULLIFY(netdef->modem_params.sim_operator_id); FREE_AND_NULLIFY(netdef->modem_params.username); memset(&netdef->modem_params, 0, sizeof(netdef->modem_params)); netdef->bridge_hairpin = NETPLAN_TRISTATE_UNSET; netdef->bridge_learning = NETPLAN_TRISTATE_UNSET; netdef->bridge_neigh_suppress = NETPLAN_TRISTATE_UNSET; FREE_AND_NULLIFY(netdef->bridge_params.ageing_time); FREE_AND_NULLIFY(netdef->bridge_params.forward_delay); FREE_AND_NULLIFY(netdef->bridge_params.hello_time); FREE_AND_NULLIFY(netdef->bridge_params.max_age); memset(&netdef->bridge_params, 0, sizeof(netdef->bridge_params)); netdef->custom_bridging = FALSE; FREE_AND_NULLIFY(netdef->tunnel.local_ip); FREE_AND_NULLIFY(netdef->tunnel.remote_ip); FREE_AND_NULLIFY(netdef->tunnel.input_key); FREE_AND_NULLIFY(netdef->tunnel.output_key); FREE_AND_NULLIFY(netdef->tunnel.private_key); memset(&netdef->tunnel, 0, sizeof(netdef->tunnel)); netdef->tunnel.mode = NETPLAN_TUNNEL_MODE_UNKNOWN; reset_auth_settings(&netdef->auth); netdef->has_auth = FALSE; netdef->sriov_link = NULL; netdef->sriov_vlan_filter = FALSE; netdef->sriov_explicit_vf_count = G_MAXUINT; /* 0 is a valid number of VFs */ FREE_AND_NULLIFY(netdef->embedded_switch_mode); reset_ovs_settings(&netdef->ovs_settings); reset_backend_settings(&netdef->backend_settings); netdef->has_backend_settings_nm = FALSE; FREE_AND_NULLIFY(netdef->filepath); netdef->tunnel_ttl = 0; FREE_AND_NULLIFY(netdef->activation_mode); netdef->ignore_carrier = FALSE; reset_private_netdef_data(netdef->_private); FREE_AND_NULLIFY(netdef->_private); netdef->receive_checksum_offload = NETPLAN_TRISTATE_UNSET; netdef->transmit_checksum_offload = NETPLAN_TRISTATE_UNSET; netdef->tcp_segmentation_offload = NETPLAN_TRISTATE_UNSET; netdef->tcp6_segmentation_offload = NETPLAN_TRISTATE_UNSET; netdef->generic_segmentation_offload = NETPLAN_TRISTATE_UNSET; netdef->generic_receive_offload = NETPLAN_TRISTATE_UNSET; netdef->large_receive_offload = NETPLAN_TRISTATE_UNSET; netdef->ib_mode = NETPLAN_IB_MODE_KERNEL; netdef->tunnel_private_key_flags = NETPLAN_KEY_FLAG_NONE; netdef->veth_peer_link = NULL; } void clear_netdef_from_list(void *def) { reset_netdef((NetplanNetDefinition *)def, NETPLAN_DEF_TYPE_NONE, NETPLAN_BACKEND_NONE); g_free(def); } NetplanState* netplan_state_new() { NetplanState* np_state = g_new0(NetplanState, 1); netplan_state_reset(np_state); return np_state; } void netplan_state_clear(NetplanState** np_state_p) { g_assert(np_state_p); NetplanState* np_state = *np_state_p; *np_state_p = NULL; netplan_state_reset(np_state); g_free(np_state); } void netplan_state_reset(NetplanState* np_state) { g_assert(np_state != NULL); /* As stated in the netplan_state definition, netdefs_ordered is the collection * owning the allocated definitions, whereas netdefs only has "weak" pointers. * As such, we can destroy it without having to worry about freeing memory. */ if(np_state->netdefs) { g_hash_table_destroy(np_state->netdefs); np_state->netdefs = NULL; } /* Here on the contrary we have to release the memory */ if(np_state->netdefs_ordered) { g_clear_list(&np_state->netdefs_ordered, clear_netdef_from_list); np_state->netdefs_ordered = NULL; } np_state->backend = NETPLAN_BACKEND_NONE; reset_ovs_settings(&np_state->ovs_settings); if (np_state->sources) { /* Properly configured at creation to clean up after itself. */ g_hash_table_destroy(np_state->sources); np_state->sources = NULL; } if (np_state->global_renderer) { g_hash_table_destroy(np_state->global_renderer); np_state->global_renderer = NULL; } } NetplanBackend netplan_state_get_backend(const NetplanState* np_state) { g_assert(np_state); return np_state->backend; } guint netplan_state_get_netdefs_size(const NetplanState* np_state) { g_assert(np_state); return np_state->netdefs ? g_hash_table_size(np_state->netdefs) : 0; } void access_point_clear(NetplanWifiAccessPoint** ap, NetplanBackend backend) { NetplanWifiAccessPoint* obj = *ap; if (!obj) return; *ap = NULL; free_access_point(NULL, obj, &backend); } #define CLEAR_FROM_FREE(free_fn, clear_fn, type) void clear_fn(type** dest) \ { \ type* obj; \ if (!dest || !(*dest)) return; \ obj = *dest; \ *dest = NULL; \ free_fn(obj);\ } CLEAR_FROM_FREE(free_wireguard_peer, wireguard_peer_clear, NetplanWireguardPeer); CLEAR_FROM_FREE(free_ip_rules, ip_rule_clear, NetplanIPRule); CLEAR_FROM_FREE(free_route, route_clear, NetplanIPRoute); CLEAR_FROM_FREE(free_address_options, address_options_clear, NetplanAddressOptions); NetplanNetDefinition* netplan_state_get_netdef(const NetplanState* np_state, const char* id) { g_assert(np_state); if (!np_state->netdefs) return NULL; return g_hash_table_lookup(np_state->netdefs, id); } ssize_t netplan_netdef_get_filepath(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size) { g_assert(netdef); return netplan_copy_string(netdef->filepath, out_buffer, out_buf_size); } NetplanBackend netplan_netdef_get_backend(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->backend; } NetplanDefType netplan_netdef_get_type(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->type; } ssize_t netplan_netdef_get_id(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size) { g_assert(netdef); return netplan_copy_string(netdef->id, out_buffer, out_buf_size); } NetplanNetDefinition* netplan_netdef_get_vlan_link(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->vlan_link; } NetplanNetDefinition* netplan_netdef_get_sriov_link(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->sriov_link; } NetplanNetDefinition* netplan_netdef_get_bridge_link(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->bridge_link; } NetplanNetDefinition* netplan_netdef_get_vrf_link(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->vrf_link; } NetplanNetDefinition* netplan_netdef_get_bond_link(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->bond_link; } NetplanNetDefinition* netplan_netdef_get_peer_link(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->peer_link; } gboolean netplan_state_has_nondefault_globals(const NetplanState* np_state) { return (np_state->backend != NETPLAN_BACKEND_NONE) || has_openvswitch(&np_state->ovs_settings, NETPLAN_BACKEND_NONE, NULL); } ssize_t _netplan_netdef_get_embedded_switch_mode(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size) { g_assert(netdef); return netplan_copy_string(netdef->embedded_switch_mode, out_buffer, out_buf_size); } gboolean _netplan_netdef_get_delay_virtual_functions_rebind(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->sriov_delay_virtual_functions_rebind; } gboolean netplan_netdef_has_match(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->has_match; } gboolean _netplan_netdef_get_sriov_vlan_filter(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->sriov_vlan_filter; } guint _netplan_netdef_get_vlan_id(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->vlan_id; } gboolean _netplan_netdef_get_critical(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->critical; } gboolean _netplan_netdef_get_optional(const NetplanNetDefinition* netdef) { g_assert(netdef); return netdef->optional; } gboolean _netplan_netdef_is_trivial_compound_itf(const NetplanNetDefinition* netdef) { g_assert(netdef); if (netdef->type == NETPLAN_DEF_TYPE_BOND) return !complex_object_is_dirty(netdef, &netdef->bond_params, sizeof(netdef->bond_params)); else if (netdef->type == NETPLAN_DEF_TYPE_BRIDGE) return !complex_object_is_dirty(netdef, &netdef->bridge_params, sizeof(netdef->bridge_params)); return FALSE; } ssize_t _netplan_netdef_get_bond_mode(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size) { g_assert(netdef); if (netdef->type == NETPLAN_DEF_TYPE_BOND && netdef->bond_params.mode) return netplan_copy_string(netdef->bond_params.mode, out_buffer, out_buf_size); else return FALSE; } netplan-1.0/src/util-internal.h000066400000000000000000000122621457004145200165470ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * Author: Martin Pitt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "types-internal.h" #include #include "netplan.h" #define SET_OPT_OUT_PTR(ptr,val) { if (ptr) *ptr = val; } #define __unused __attribute__((unused)) extern GHashTable* wifi_frequency_24; extern GHashTable* wifi_frequency_5; NETPLAN_INTERNAL void _netplan_safe_mkdir_p_dir(const char* file_path); NETPLAN_INTERNAL void _netplan_g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix); NETPLAN_INTERNAL void _netplan_unlink_glob(const char* rootdir, const char* _glob); NETPLAN_INTERNAL int _netplan_find_yaml_glob(const char* rootdir, glob_t* out_glob); const char* get_global_network(int ip_family); const char* get_unspecified_address(int ip_family); int wifi_get_freq24(int channel); int wifi_get_freq5(int channel); gchar* systemd_escape(char* string); #define OPENVSWITCH_OVS_VSCTL "/usr/bin/ovs-vsctl" void mark_data_as_dirty(NetplanParser* npp, const void* data_ptr); const char* tunnel_mode_to_string(NetplanTunnelMode mode); NetplanBackend get_default_backend_for_type(NetplanBackend global_backend, NetplanDefType type); NetplanNetDefinition* netplan_netdef_new(NetplanParser* npp, const char* id, NetplanDefType type, NetplanBackend renderer); const char * netplan_parser_get_filename(NetplanParser* npp); gboolean has_openvswitch(const NetplanOVSSettings* ovs, NetplanBackend backend, GHashTable *ovs_ports); ssize_t netplan_copy_string(const char* input, char* out_buffer, size_t out_size); gboolean complex_object_is_dirty(const NetplanNetDefinition* def, const void* obj, size_t obj_size); gboolean is_multicast_address(const char*); NETPLAN_INTERNAL int _netplan_state_get_vf_count_for_def(const NetplanState* np_state, const NetplanNetDefinition* netdef, NetplanError** error); NETPLAN_INTERNAL gboolean _netplan_netdef_get_sriov_vlan_filter(const NetplanNetDefinition* netdef); NETPLAN_INTERNAL gboolean _netplan_netdef_get_critical(const NetplanNetDefinition* netdef); NETPLAN_INTERNAL gboolean _netplan_netdef_get_optional(const NetplanNetDefinition* netdef); NETPLAN_INTERNAL ssize_t _netplan_netdef_get_embedded_switch_mode(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size); NETPLAN_INTERNAL gboolean _netplan_netdef_get_delay_virtual_functions_rebind(const NetplanNetDefinition* netdef); NETPLAN_INTERNAL guint _netplan_netdef_get_vlan_id(const NetplanNetDefinition* netdef); NETPLAN_INTERNAL ssize_t _netplan_netdef_get_bond_mode(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size); NETPLAN_INTERNAL gboolean _netplan_netdef_is_trivial_compound_itf(const NetplanNetDefinition* netdef); gboolean is_route_present(const NetplanNetDefinition* netdef, const NetplanIPRoute* route); gboolean is_route_rule_present(const NetplanNetDefinition* netdef, const NetplanIPRule* rule); gboolean is_string_in_array(GArray* array, const char* value); gboolean _is_auth_key_management_psk(const NetplanAuthenticationSettings* auth); gboolean _is_macaddress_special_nm_option(const char* value); gboolean _is_macaddress_special_nd_option(const char* value); gboolean _is_valid_macaddress(const char* value); NETPLAN_INTERNAL struct address_iter* _netplan_netdef_new_address_iter(NetplanNetDefinition* netdef); NETPLAN_INTERNAL NetplanAddressOptions* _netplan_address_iter_next(struct address_iter* it); NETPLAN_INTERNAL void _netplan_address_iter_free(struct address_iter* it); NETPLAN_INTERNAL struct nameserver_iter* _netplan_netdef_new_nameserver_iter(NetplanNetDefinition* netdef); NETPLAN_INTERNAL char* _netplan_nameserver_iter_next(struct nameserver_iter* it); NETPLAN_INTERNAL void _netplan_nameserver_iter_free(struct nameserver_iter* it); NETPLAN_INTERNAL struct nameserver_iter* _netplan_netdef_new_search_domain_iter(NetplanNetDefinition* netdef); NETPLAN_INTERNAL char* _netplan_search_domain_iter_next(struct nameserver_iter* it); NETPLAN_INTERNAL void _netplan_search_domain_iter_free(struct nameserver_iter* it); NETPLAN_INTERNAL struct route_iter* _netplan_netdef_new_route_iter(NetplanNetDefinition* netdef); NETPLAN_INTERNAL NetplanIPRoute* _netplan_route_iter_next(struct route_iter* it); NETPLAN_INTERNAL void _netplan_route_iter_free(struct route_iter* it); NETPLAN_INTERNAL struct netdef_pertype_iter* _netplan_state_new_netdef_pertype_iter(NetplanState* np_state, const char* def_type); NETPLAN_INTERNAL NetplanNetDefinition* _netplan_netdef_pertype_iter_next(struct netdef_pertype_iter* it); NETPLAN_INTERNAL void _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it); netplan-1.0/src/util.c000066400000000000000000001115011457004145200147240ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * Author: Martin Pitt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include "util.h" #include "util-internal.h" #include "netplan.h" #include "parse.h" #include "names.h" #include "yaml-helpers.h" GHashTable* wifi_frequency_24; GHashTable* wifi_frequency_5; const gchar* FALLBACK_FILENAME = "70-netplan-set.yaml"; typedef struct netplan_state_iterator RealStateIter; /** * Create the parent directories of given file path. Exit program on failure. */ void _netplan_safe_mkdir_p_dir(const char* file_path) { g_autofree char* dir = g_path_get_dirname(file_path); if (g_mkdir_with_parents(dir, 0755) < 0) { g_fprintf(stderr, "ERROR: cannot create directory %s: %m\n", dir); exit(1); } } /** * Write a GString to a file and free it. Create necessary parent directories * and exit with error message on error. * @s: #GString whose contents to write. Will be fully freed afterwards. * @rootdir: optional rootdir (@NULL means "/") * @path: path of file to write (@rootdir will be prepended) * @suffix: optional suffix to append to path */ void _netplan_g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix) { g_autofree char* full_path = NULL; g_autofree char* path_suffix = NULL; g_autofree char* contents = g_string_free(s, FALSE); GError* error = NULL; path_suffix = g_strjoin(NULL, path, suffix, NULL); full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL); _netplan_safe_mkdir_p_dir(full_path); if (!g_file_set_contents(full_path, contents, -1, &error)) { /* the mkdir() just succeeded, there is no sensible * method to test this without root privileges, bind mounts, and * simulating ENOSPC */ // LCOV_EXCL_START g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message); exit(1); // LCOV_EXCL_STOP } } /** * Remove all files matching given glob. */ void _netplan_unlink_glob(const char* rootdir, const char* _glob) { glob_t gl; int rc; g_autofree char* rglob = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, _glob, NULL); rc = glob(rglob, GLOB_BRACE, NULL, &gl); if (rc != 0 && rc != GLOB_NOMATCH) { // LCOV_EXCL_START g_fprintf(stderr, "failed to glob for %s: %m\n", rglob); return; // LCOV_EXCL_STOP } for (size_t i = 0; i < gl.gl_pathc; ++i) unlink(gl.gl_pathv[i]); globfree(&gl); } /** * Return a glob of all *.yaml files in /{lib,etc,run}/netplan/ (in this order) */ int _netplan_find_yaml_glob(const char* rootdir, glob_t* out_glob) { int rc; g_autofree char* rglob = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, "{lib,etc,run}/netplan/*.yaml", NULL); rc = glob(rglob, GLOB_BRACE, NULL, out_glob); if (rc != 0 && rc != GLOB_NOMATCH) { // LCOV_EXCL_START g_fprintf(stderr, "failed to glob for %s: %m\n", rglob); return 1; // LCOV_EXCL_STOP } return 0; } gboolean netplan_util_create_yaml_patch(const char* conf_obj_path, const char* obj_payload, int output_fd, GError** error) { yaml_emitter_t emitter; yaml_parser_t parser; yaml_event_t event; int token_depth = 0; int out_dup = -1; FILE* out_stream = NULL; int ret = FALSE; out_dup = dup(output_fd); if (out_dup < 0) goto file_error; // LCOV_EXCL_LINE out_stream = fdopen(out_dup, "w"); if (!out_stream) goto file_error; // LCOV_EXCL_LINE yaml_emitter_initialize(&emitter); yaml_parser_initialize(&parser); yaml_emitter_set_output_file(&emitter, out_stream); yaml_stream_start_event_initialize(&event, YAML_UTF8_ENCODING); if (!yaml_emitter_emit(&emitter, &event)) goto err_path; // LCOV_EXCL_LINE yaml_document_start_event_initialize(&event, NULL, NULL, NULL, 1); if (!yaml_emitter_emit(&emitter, &event)) goto err_path; // LCOV_EXCL_LINE char **tokens = g_strsplit_set(conf_obj_path, "\t", -1); for (; tokens[token_depth] != NULL; token_depth++) { YAML_MAPPING_OPEN(&event, &emitter); YAML_SCALAR_PLAIN(&event, &emitter, tokens[token_depth]); } g_strfreev(tokens); yaml_parser_set_input_string(&parser, (const unsigned char *)obj_payload, strlen(obj_payload)); while (TRUE) { if (!yaml_parser_parse(&parser, &event)) { g_set_error(error, NETPLAN_FORMAT_ERROR, NETPLAN_ERROR_FORMAT_INVALID_YAML, "Error parsing YAML: %s", parser.problem); goto cleanup; } if (event.type == YAML_STREAM_END_EVENT || event.type == YAML_DOCUMENT_END_EVENT) break; switch (event.type) { case YAML_STREAM_START_EVENT: case YAML_DOCUMENT_START_EVENT: break; case YAML_MAPPING_START_EVENT: YAML_MAPPING_OPEN(&event, &emitter); break; case YAML_SEQUENCE_START_EVENT: YAML_SEQUENCE_OPEN(&event, &emitter); break; default: if (!yaml_emitter_emit(&emitter, &event)) goto err_path; // LCOV_EXCL_LINE } } for (; token_depth > 0; token_depth--) YAML_MAPPING_CLOSE(&event, &emitter); yaml_document_end_event_initialize(&event, 1); if (!yaml_emitter_emit(&emitter, &event)) goto err_path; // LCOV_EXCL_LINE yaml_stream_end_event_initialize(&event); if (!yaml_emitter_emit(&emitter, &event)) goto err_path; // LCOV_EXCL_LINE yaml_emitter_flush(&emitter); fflush(out_stream); ret = TRUE; goto cleanup; // LCOV_EXCL_START err_path: g_set_error(error, NETPLAN_EMITTER_ERROR, NETPLAN_ERROR_YAML_EMITTER, "Error generating YAML: %s", emitter.problem); ret = FALSE; // LCOV_EXCL_STOP cleanup: /* also closes the dup FD */ fclose(out_stream); yaml_emitter_delete(&emitter); yaml_parser_delete(&parser); return ret; // LCOV_EXCL_START file_error: g_set_error(error, NETPLAN_FILE_ERROR, errno, "Error when opening FD %d: %m", output_fd); if (out_dup >= 0) close(out_dup); return FALSE; // LCOV_EXCL_STOP } STATIC gboolean copy_yaml_subtree(yaml_parser_t *parser, yaml_emitter_t *emitter, GError** error) { yaml_event_t event; int map_count = 0, seq_count = 0; do { if (!yaml_parser_parse(parser, &event)) { g_set_error(error, NETPLAN_FORMAT_ERROR, NETPLAN_ERROR_FORMAT_INVALID_YAML, "Error parsing YAML: %s", parser->problem); return FALSE; } switch (event.type) { case YAML_MAPPING_START_EVENT: map_count++; break; case YAML_SEQUENCE_START_EVENT: seq_count++; break; case YAML_MAPPING_END_EVENT: map_count--; break; case YAML_SEQUENCE_END_EVENT: seq_count--; break; default: break; } if (emitter && !yaml_emitter_emit(emitter, &event)) { // LCOV_EXCL_START g_set_error(error, NETPLAN_PARSER_ERROR, NETPLAN_ERROR_INVALID_YAML, "Error emitting YAML: %s", emitter->problem); return FALSE; // LCOV_EXCL_STOP } } while (map_count || seq_count); return TRUE; } /** * Given a YAML tree and a YAML path (array of keys with NULL as the last array element), * emits the subtree matching the path, while emitting the rest of the data into the void. */ STATIC gboolean emit_yaml_subtree(yaml_parser_t *parser, yaml_emitter_t *emitter, char** yaml_path, GError** error) { yaml_event_t event; /* If the path component is NULL, we're done with the trimming, we can just copy the whole subtree */ if (!(*yaml_path)) return copy_yaml_subtree(parser, emitter, error); if (!yaml_parser_parse(parser, &event)) goto parser_err_path; // LCOV_EXCL_LINE if (event.type != YAML_MAPPING_START_EVENT) { g_set_error(error, NETPLAN_FORMAT_ERROR, NETPLAN_ERROR_FORMAT_INVALID_YAML, "Unexpected YAML structure found"); return FALSE; } while (TRUE) { if (!yaml_parser_parse(parser, &event)) goto parser_err_path; if (event.type == YAML_MAPPING_END_EVENT) break; if (g_strcmp0(*yaml_path, (char*)event.data.scalar.value) == 0) { /* Go further down, popping the component we just used from the path */ if (!emit_yaml_subtree(parser, emitter, yaml_path+1, error)) return FALSE; } else { /* We're out of the path, so we trim the branch by "emitting" the data into a NULL emitter */ if (!copy_yaml_subtree(parser, NULL, error)) return FALSE; } } return TRUE; parser_err_path: g_set_error(error, NETPLAN_FORMAT_ERROR, NETPLAN_ERROR_FORMAT_INVALID_YAML, "Error parsing YAML: %s", parser->problem); return FALSE; } gboolean netplan_util_dump_yaml_subtree(const char* prefix, int input_fd, int output_fd, NetplanError** error) { gboolean ret = TRUE; char **yaml_path = NULL; yaml_emitter_t emitter; yaml_parser_t parser; yaml_event_t event; int in_dup = -1, out_dup = -1; FILE* input = NULL; FILE* output = NULL; in_dup = dup(input_fd); if (in_dup < 0) goto file_error; // LCOV_EXCL_LINE out_dup = dup(output_fd); if (out_dup < 0) goto file_error; // LCOV_EXCL_LINE input = fdopen(in_dup, "r"); output = fdopen(out_dup, "w"); if (!input || !output) goto file_error; if (fseek(input, 0, SEEK_SET) < 0) goto file_error; // LCOV_EXCL_LINE yaml_path = g_strsplit(prefix, "\t", -1); yaml_parser_initialize(&parser); yaml_parser_set_input_file(&parser, input); yaml_emitter_initialize(&emitter); yaml_emitter_set_output_file(&emitter, output); /* Copy over the stream and document start events */ for (int i = 0; i < 2; ++i) { if (!yaml_parser_parse(&parser, &event)) goto parser_err_path; // LCOV_EXCL_LINE if (!yaml_emitter_emit(&emitter, &event)) goto err_path; // LCOV_EXCL_LINE } if (!emit_yaml_subtree(&parser, &emitter, yaml_path, error)) { ret = FALSE; goto cleanup; } if (emitter.events.head != emitter.events.tail) { YAML_NULL_PLAIN(&event, &emitter); } do { if (!yaml_parser_parse(&parser, &event)) goto parser_err_path; // LCOV_EXCL_LINE if (!yaml_emitter_emit(&emitter, &event)) goto err_path; // LCOV_EXCL_LINE } while (!parser.stream_end_produced); goto cleanup; file_error: g_set_error(error, NETPLAN_FILE_ERROR, errno, "%m"); ret = FALSE; goto cleanup; // LCOV_EXCL_START parser_err_path: g_set_error(error, NETPLAN_FORMAT_ERROR, NETPLAN_ERROR_FORMAT_INVALID_YAML, "Error parsing YAML: %s", parser.problem); ret = FALSE; goto cleanup; err_path: g_set_error(error, NETPLAN_EMITTER_ERROR, NETPLAN_ERROR_YAML_EMITTER, "Error generating YAML: %s", emitter.problem); ret = FALSE; // LCOV_EXCL_STOP cleanup: if (input) fclose(input); else if (in_dup >= 0) close(in_dup); if (output) fclose(output); else if (out_dup >= 0) close(out_dup); if (yaml_path) g_strfreev(yaml_path); return ret; } /** * Get the frequency of a given 2.4GHz WiFi channel */ int wifi_get_freq24(int channel) { if (channel < 1 || channel > 14) { g_fprintf(stderr, "ERROR: invalid 2.4GHz WiFi channel: %d\n", channel); exit(1); } if (!wifi_frequency_24) { wifi_frequency_24 = g_hash_table_new(g_direct_hash, g_direct_equal); /* Initialize 2.4GHz frequencies, as of: * https://en.wikipedia.org/wiki/List_of_WLAN_channels#2.4_GHz_(802.11b/g/n/ax) */ for (unsigned i = 0; i < 13; i++) { g_hash_table_insert(wifi_frequency_24, GINT_TO_POINTER(i+1), GINT_TO_POINTER(2412+i*5)); } g_hash_table_insert(wifi_frequency_24, GINT_TO_POINTER(14), GINT_TO_POINTER(2484)); } return GPOINTER_TO_INT(g_hash_table_lookup(wifi_frequency_24, GINT_TO_POINTER(channel))); } /** * Get the frequency of a given 5GHz WiFi channel */ int wifi_get_freq5(int channel) { int channels[] = { 7, 8, 9, 11, 12, 16, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 68, 96, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 132, 134, 136, 138, 140, 142, 144, 149, 151, 153, 155, 157, 159, 161, 165, 169, 173 }; gboolean found = FALSE; for (unsigned i = 0; i < sizeof(channels) / sizeof(int); i++) { if (channel == channels[i]) { found = TRUE; break; } } if (!found) { g_fprintf(stderr, "ERROR: invalid 5GHz WiFi channel: %d\n", channel); exit(1); } if (!wifi_frequency_5) { wifi_frequency_5 = g_hash_table_new(g_direct_hash, g_direct_equal); /* Initialize 5GHz frequencies, as of: * https://en.wikipedia.org/wiki/List_of_WLAN_channels#5.0_GHz_(802.11j)_WLAN * Skipping channels 183-196. They are valid only in Japan with registration needed */ for (unsigned i = 0; i < sizeof(channels) / sizeof(int); i++) { g_hash_table_insert(wifi_frequency_5, GINT_TO_POINTER(channels[i]), GINT_TO_POINTER(5000+channels[i]*5)); } } return GPOINTER_TO_INT(g_hash_table_lookup(wifi_frequency_5, GINT_TO_POINTER(channel))); } /** * Systemd-escape the given string. The caller is responsible for freeing * the allocated escaped string. */ gchar* systemd_escape(char* string) { g_autoptr(GError) err = NULL; g_autofree gchar* stderrh = NULL; gint exit_status = 0; gchar *escaped; gchar *argv[] = {"bin" "/" "systemd-escape", "--", string, NULL}; g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &escaped, &stderrh, &exit_status, &err); #if GLIB_CHECK_VERSION (2, 70, 0) g_spawn_check_wait_status(exit_status, &err); #else g_spawn_check_exit_status(exit_status, &err); #endif if (err != NULL) { // LCOV_EXCL_START g_fprintf(stderr, "failed to ask systemd to escape %s; exit %d\nstdout: '%s'\nstderr: '%s'", string, exit_status, escaped, stderrh); exit(1); // LCOV_EXCL_STOP } g_strstrip(escaped); return escaped; } gboolean netplan_delete_connection(const char* id, const char* rootdir) { g_autofree gchar* yaml_path = NULL; g_autoptr(GError) error = NULL; NetplanNetDefinition* nd = NULL; gboolean ret = TRUE; int patch_fd = -1; NetplanParser* input_parser = netplan_parser_new(); NetplanState* input_state = netplan_state_new(); NetplanParser* output_parser = NULL; NetplanState* output_state = NULL; /* parse all YAML files */ if ( !netplan_parser_load_yaml_hierarchy(input_parser, rootdir, &error) || !netplan_state_import_parser_results(input_state, input_parser, &error)) { g_fprintf(stderr, "netplan_delete_connection: Cannot parse input: %s\n", error->message); ret = FALSE; goto cleanup; } /* find specified netdef in input state */ nd = netplan_state_get_netdef(input_state, id); if (!nd) { g_fprintf(stderr, "netplan_delete_connection: Cannot delete %s, does not exist.\n", id); ret = FALSE; goto cleanup; } /* Build up a tab-separated YAML path for this Netdef (e.g. network.ethernets.eth0=...) */ yaml_path = g_strdup_printf("network\t%s\t%s", netplan_def_type_name(nd->type), id); /* create a temporary file in memory, to hold our YAML patch */ patch_fd = memfd_create("patch.yaml", 0); if (patch_fd < 0) { // LCOV_EXCL_START g_fprintf(stderr, "netplan_delete_connection: Cannot create memfd: %m\n"); ret = FALSE; goto cleanup; // LCOV_EXCL_STOP } if (!netplan_util_create_yaml_patch(yaml_path, "NULL", patch_fd, &error)) { // LCOV_EXCL_START g_fprintf(stderr, "netplan_delete_connection: Cannot create YAML patch: %s\n", error->message); ret = FALSE; goto cleanup; // LCOV_EXCL_STOP } /* Create a new parser & state to hold our output YAML, ignoring the to be * deleted Netdef from the patch */ output_parser = netplan_parser_new(); output_state = netplan_state_new(); lseek(patch_fd, 0, SEEK_SET); if ( !netplan_parser_load_nullable_fields(output_parser, patch_fd, &error) || !netplan_parser_load_yaml_hierarchy(output_parser, rootdir, &error)) { // LCOV_EXCL_START g_fprintf(stderr, "netplan_delete_connection: Cannot load output state: %s\n", error->message); ret = FALSE; goto cleanup; // LCOV_EXCL_STOP } lseek(patch_fd, 0, SEEK_SET); if (!netplan_parser_load_yaml_from_fd(output_parser, patch_fd, &error)) { // LCOV_EXCL_START g_fprintf(stderr, "netplan_delete_connection: Cannot parse YAML patch: %s\n", error->message); ret = FALSE; goto cleanup; // LCOV_EXCL_STOP } /* We're only deleting some data, so FALLBACK_FILENAME should never be created */ if ( !netplan_state_import_parser_results(output_state, output_parser, &error) || !netplan_state_update_yaml_hierarchy(output_state, FALLBACK_FILENAME, rootdir, &error)) { // LCOV_EXCL_START g_fprintf(stderr, "netplan_delete_connection: Cannot write output state: %s\n", error->message); ret = FALSE; goto cleanup; // LCOV_EXCL_STOP } cleanup: if (input_parser) netplan_parser_clear(&input_parser); if (input_state) netplan_state_clear(&input_state); if (output_parser) netplan_parser_clear(&output_parser); if (output_state) netplan_state_clear(&output_state); if (patch_fd >= 0) close(patch_fd); return ret; } /** * Extract the netplan netdef ID from a NetworkManager connection profile (keyfile), * generated by netplan. Used by the NetworkManager YAML backend. */ ssize_t netplan_get_id_from_nm_filepath(const char* filename, const char* ssid, char* out_buffer, size_t out_buf_size) { g_autofree gchar* escaped_ssid = NULL; g_autofree gchar* suffix = NULL; const char* nm_prefix = "/run/NetworkManager/system-connections/netplan-"; const char* pos = g_strrstr(filename, nm_prefix); const char* start = NULL; const char* end = NULL; gsize id_len = 0; if (!pos) return 0; if (ssid) { escaped_ssid = g_uri_escape_string(ssid, NULL, TRUE); suffix = g_strdup_printf("-%s.nmconnection", escaped_ssid); end = g_strrstr(filename, suffix); } else end = g_strrstr(filename, ".nmconnection"); if (!end) return 0; /* Move pointer to start of netplan ID inside filename string */ start = pos + strlen(nm_prefix); id_len = end - start; if (out_buf_size < id_len + 1) return NETPLAN_BUFFER_TOO_SMALL; strncpy(out_buffer, start, id_len); out_buffer[id_len] = '\0'; return id_len + 1; } ssize_t netplan_netdef_get_output_filename(const NetplanNetDefinition* netdef, const char* ssid, char* out_buffer, size_t out_buf_size) { g_autofree gchar* conf_path = NULL; if (netdef->backend == NETPLAN_BACKEND_NM) { if (ssid) { g_autofree char* escaped_ssid = g_uri_escape_string(ssid, NULL, TRUE); conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", netdef->id, "-", escaped_ssid, ".nmconnection", NULL); } else { conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", netdef->id, ".nmconnection", NULL); } } else if (netdef->backend == NETPLAN_BACKEND_NETWORKD || netdef->backend == NETPLAN_BACKEND_OVS) { conf_path = g_strjoin(NULL, "/run/systemd/network/10-netplan-", netdef->id, ".network", NULL); } if (conf_path) return netplan_copy_string(conf_path, out_buffer, out_buf_size); return 0; } gboolean netplan_parser_load_yaml_hierarchy(NetplanParser* npp, const char* rootdir, GError** error) { glob_t gl; /* Files with asciibetically higher names override/append settings from * earlier ones (in all config dirs); files in /run/netplan/ * shadow files in /etc/netplan/ which shadow files in /lib/netplan/. * To do that, we put all found files in a hash table, then sort it by * file name, and add the entries from /run after the ones from /etc * and those after the ones from /lib. */ if (_netplan_find_yaml_glob(rootdir, &gl) != 0) return FALSE; // LCOV_EXCL_LINE /* keys are strdup()ed, free them; values point into the glob_t, don't free them */ g_autoptr(GHashTable) configs = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); g_autoptr(GList) config_keys = NULL; for (size_t i = 0; i < gl.gl_pathc; ++i) g_hash_table_insert(configs, g_path_get_basename(gl.gl_pathv[i]), gl.gl_pathv[i]); config_keys = g_list_sort(g_hash_table_get_keys(configs), (GCompareFunc) strcmp); for (GList* i = config_keys; i != NULL; i = i->next) if (!netplan_parser_load_yaml(npp, g_hash_table_lookup(configs, i->data), error)) { globfree(&gl); return FALSE; } globfree(&gl); return TRUE; } /** * Get a static string describing the default global network * for a given address family. */ const char * get_global_network(int ip_family) { g_assert(ip_family == AF_INET || ip_family == AF_INET6); if (ip_family == AF_INET) return "0.0.0.0/0"; else return "::/0"; } const char * get_unspecified_address(int ip_family) { g_assert(ip_family == AF_INET || ip_family == AF_INET6); return (ip_family == AF_INET) ? "0.0.0.0" : "::"; } struct address_iter* _netplan_netdef_new_address_iter(NetplanNetDefinition* netdef) { struct address_iter* it = g_malloc0(sizeof(struct address_iter)); it->ip4_index = 0; it->ip6_index = 0; it->address_options_index = 0; it->netdef = netdef; it->last_address = NULL; return it; } /* * The netdef address iterator produces NetplanAddressOptions * for all the addresses stored in ip4_address, ip6_address and * address_options (in this order). * * The current value produced by the iterator is saved in it->last_address * and the previous one is released. The idea is to not leave to the caller * the responsibility of releasing each value. The very last value * will be released either when the iterator is destroyed or when there is * nothing else to be produced and the iterator was called one last time. */ NetplanAddressOptions* _netplan_address_iter_next(struct address_iter* it) { NetplanAddressOptions* options = NULL; if (it->last_address) { free_address_options(it->last_address); it->last_address = NULL; } if (it->netdef->ip4_addresses && it->ip4_index < it->netdef->ip4_addresses->len) { options = g_malloc0(sizeof(NetplanAddressOptions)); options->address = g_strdup(g_array_index(it->netdef->ip4_addresses, char*, it->ip4_index++)); it->last_address = options; return options; } if (it->netdef->ip6_addresses && it->ip6_index < it->netdef->ip6_addresses->len) { options = g_malloc0(sizeof(NetplanAddressOptions)); options->address = g_strdup(g_array_index(it->netdef->ip6_addresses, char*, it->ip6_index++)); it->last_address = options; return options; } if (it->netdef->address_options && it->address_options_index < it->netdef->address_options->len) { options = g_malloc0(sizeof(NetplanAddressOptions)); NetplanAddressOptions* netdef_options = g_array_index(it->netdef->address_options, NetplanAddressOptions*, it->address_options_index++); options->address = g_strdup(netdef_options->address); options->lifetime = g_strdup(netdef_options->lifetime); options->label = g_strdup(netdef_options->label); it->last_address = options; return options; } return options; } void _netplan_address_iter_free(struct address_iter* it) { if (it->last_address) free_address_options(it->last_address); g_free(it); } struct nameserver_iter* _netplan_netdef_new_nameserver_iter(NetplanNetDefinition* netdef) { struct nameserver_iter* it = g_malloc0(sizeof(struct nameserver_iter)); it->ip4_index = 0; it->ip6_index = 0; it->netdef = netdef; return it; } char* _netplan_nameserver_iter_next(struct nameserver_iter* it) { if (it->netdef->ip4_nameservers && it->ip4_index < it->netdef->ip4_nameservers->len) { return g_array_index(it->netdef->ip4_nameservers, char*, it->ip4_index++); } if (it->netdef->ip6_nameservers && it->ip6_index < it->netdef->ip6_nameservers->len) { return g_array_index(it->netdef->ip6_nameservers, char*, it->ip6_index++); } return NULL; } void _netplan_nameserver_iter_free(struct nameserver_iter* it) { g_free(it); } struct nameserver_iter* _netplan_netdef_new_search_domain_iter(NetplanNetDefinition* netdef) { struct nameserver_iter* it = g_malloc0(sizeof(struct nameserver_iter)); it->search_index = 0; it->netdef = netdef; return it; } char* _netplan_search_domain_iter_next(struct nameserver_iter* it) { if (it->netdef->search_domains && it->search_index < it->netdef->search_domains->len) { return g_array_index(it->netdef->search_domains, char*, it->search_index++); } return NULL; } void _netplan_search_domain_iter_free(struct nameserver_iter* it) { g_free(it); } struct route_iter* _netplan_netdef_new_route_iter(NetplanNetDefinition* netdef) { struct route_iter* it = g_malloc0(sizeof(struct route_iter)); it->route_index = 0; it->netdef = netdef; return it; } NetplanIPRoute* _netplan_route_iter_next(struct route_iter* it) { if (it->netdef->routes && it->route_index < it->netdef->routes->len) return g_array_index(it->netdef->routes, NetplanIPRoute*, it->route_index++); return NULL; } void _netplan_route_iter_free(struct route_iter* it) { g_free(it); } struct netdef_pertype_iter { NetplanDefType type; GHashTableIter iter; NetplanState* np_state; }; struct netdef_pertype_iter* _netplan_state_new_netdef_pertype_iter(NetplanState* np_state, const char* def_type) { NetplanDefType type = def_type ? netplan_def_type_from_name(def_type) : NETPLAN_DEF_TYPE_NONE; struct netdef_pertype_iter* iter = g_malloc0(sizeof(*iter)); iter->type = type; iter->np_state = np_state; if (np_state->netdefs) g_hash_table_iter_init(&iter->iter, np_state->netdefs); return iter; } NetplanNetDefinition* _netplan_netdef_pertype_iter_next(struct netdef_pertype_iter* it) { gpointer key, value; if (!it->np_state->netdefs) return NULL; while (g_hash_table_iter_next(&it->iter, &key, &value)) { NetplanNetDefinition* netdef = value; if (netdef->type == NETPLAN_DEF_TYPE_NM_PLACEHOLDER_) continue; if (it->type == NETPLAN_DEF_TYPE_NONE || netdef->type == it->type) return netdef; } return NULL; } void _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it) { g_free(it); } gboolean has_openvswitch(const NetplanOVSSettings* ovs, NetplanBackend backend, GHashTable *ovs_ports) { return (ovs_ports && g_hash_table_size(ovs_ports) > 0) || (ovs->external_ids && g_hash_table_size(ovs->external_ids) > 0) || (ovs->other_config && g_hash_table_size(ovs->other_config) > 0) || ovs->lacp || ovs->fail_mode || ovs->mcast_snooping || ovs->rstp || ovs->protocols || (ovs->ssl.ca_certificate || ovs->ssl.client_certificate || ovs->ssl.client_key) || (ovs->controller.connection_mode || ovs->controller.addresses) || backend == NETPLAN_BACKEND_OVS; } void mark_data_as_dirty(NetplanParser* npp, const void* data_ptr) { // We don't support dirty tracking for globals yet. if (!npp->current.netdef) return; if (!npp->current.netdef->_private) npp->current.netdef->_private = g_new0(struct private_netdef_data, 1); if (!npp->current.netdef->_private->dirty_fields) npp->current.netdef->_private->dirty_fields = g_hash_table_new(g_direct_hash, g_direct_equal); g_hash_table_insert(npp->current.netdef->_private->dirty_fields, (void*)data_ptr, (void*)data_ptr); } gboolean complex_object_is_dirty(const NetplanNetDefinition* def, const void* obj, size_t obj_size) { const char* ptr = obj; if (def->_private == NULL || def->_private->dirty_fields == NULL) return FALSE; for (size_t i = 0; i < obj_size; ++i) { if (g_hash_table_contains(def->_private->dirty_fields, ptr+i)) return TRUE; } return FALSE; } /** * Copy a NUL-terminated string into a sized buffer, and returns the size of * the copied string, including the final NUL character. If the buffer is too * small, returns NETPLAN_BUFFER_TOO_SMALL instead. * * In all cases the contents of the output buffer will be entirely overwritten, * except if the input string is NULL. Notably, if the buffer is too small its * content will NOT be NUL-terminated. * * @input: the input string * @out_buffer: a pointer to a buffer into which we want to copy the string * @out_size: the size of the output buffer */ ssize_t netplan_copy_string(const char* input, char* out_buffer, size_t out_size) { if (input == NULL) return 0; // LCOV_EXCL_LINE char* end = stpncpy(out_buffer, input, out_size); // If it point to the first byte past the buffer, we don't have enough // space in the buffer. size_t len = end - out_buffer; if (len == out_size) return NETPLAN_BUFFER_TOO_SMALL; return end - out_buffer + 1; } gboolean netplan_netdef_match_interface(const NetplanNetDefinition* netdef, const char* name, const char* mac, const char* driver_name) { if (!netdef->has_match) return !g_strcmp0(name, netdef->id); if (netdef->match.mac) { if (g_ascii_strcasecmp(netdef->match.mac, mac)) return FALSE; } if (netdef->match.original_name) { if (!name || fnmatch(netdef->match.original_name, name, 0)) return FALSE; } if (netdef->match.driver) { gboolean matches_driver = FALSE; char** tokens; if (!driver_name) return FALSE; tokens = g_strsplit(netdef->match.driver, "\t", -1); for (char** it = tokens; *it; it++) { if (fnmatch(*it, driver_name, 0) == 0) { matches_driver = TRUE; break; } } g_strfreev(tokens); return matches_driver; } return TRUE; } ssize_t netplan_netdef_get_set_name(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size) { return netplan_copy_string(netdef->set_name, out_buffer, out_buf_size); } ssize_t netplan_netdef_get_macaddress(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buf_size) { return netplan_copy_string(netdef->set_mac, out_buffer, out_buf_size); } gboolean netplan_netdef_get_dhcp4(const NetplanNetDefinition* netdef) { return netdef->dhcp4; } gboolean netplan_netdef_get_dhcp6(const NetplanNetDefinition* netdef) { return netdef->dhcp6; } gboolean netplan_netdef_get_link_local_ipv4(const NetplanNetDefinition* netdef) { return netdef->linklocal.ipv4; } gboolean netplan_netdef_get_link_local_ipv6(const NetplanNetDefinition* netdef) { return netdef->linklocal.ipv6; } gboolean is_multicast_address(const char* address) { struct in_addr a4; struct in6_addr a6; if (inet_pton(AF_INET, address, &a4) > 0) { if (ntohl(a4.s_addr) >> 28 == 0b1110) /* 224.0.0.0/4 */ return TRUE; } else if (inet_pton(AF_INET6, address, &a6) > 0) { if (a6.s6_addr[0] == 0xff) /* FF00::/8 */ return TRUE; } return FALSE; } void netplan_state_iterator_init(const NetplanState* np_state, NetplanStateIterator* iter) { g_assert(iter); RealStateIter* _iter = (RealStateIter*) iter; _iter->next = g_list_first(np_state->netdefs_ordered); } NetplanNetDefinition* netplan_state_iterator_next(NetplanStateIterator* iter) { NetplanNetDefinition* netdef = NULL; RealStateIter* _iter = (RealStateIter*) iter; if (_iter && _iter->next) { netdef = _iter->next->data; _iter->next = g_list_next(_iter->next); } return netdef; } gboolean netplan_state_iterator_has_next(const NetplanStateIterator* iter) { RealStateIter* _iter = (RealStateIter*) iter; if (!_iter) return FALSE; return _iter->next != NULL; } STATIC const char* normalize_ip_address(const char* addr, const guint family) { if (!g_strcmp0(addr, "default")) { if (family == AF_INET) return "0.0.0.0/0"; else return "::/0"; } return addr; } /* * Returns true if a route already exists in the netdef routes list. * * We consider a route a duplicate if it is in the same table, has the same metric, * src, to, via and family values. * * XXX: in the future we could add a route "key" to a hash set so this verification could * be done faster. */ gboolean is_route_present(const NetplanNetDefinition* netdef, const NetplanIPRoute* route) { const GArray* routes = netdef->routes; for (guint i = 0; i < routes->len; i++) { const NetplanIPRoute* entry = g_array_index(routes, NetplanIPRoute*, i); if ( entry->family == route->family && entry->table == route->table && entry->metric == route->metric && g_strcmp0(entry->from, route->from) == 0 && g_strcmp0(normalize_ip_address(entry->to, entry->family), normalize_ip_address(route->to, route->family)) == 0 && g_strcmp0(entry->via, route->via) == 0 ) return TRUE; } return FALSE; } /* * Returns true if a policy rule already exists in the netdef rules list. */ gboolean is_route_rule_present(const NetplanNetDefinition* netdef, const NetplanIPRule* rule) { const GArray* rules = netdef->ip_rules; for (guint i = 0; i < rules->len; i++) { const NetplanIPRule* entry = g_array_index(rules, NetplanIPRule*, i); if ( entry->family == rule->family && g_strcmp0(entry->from, rule->from) == 0 && g_strcmp0(entry->to, rule->to) == 0 && entry->table == rule->table && entry->priority == rule->priority && entry->fwmark == rule->fwmark && entry->tos == rule->tos ) return TRUE; } return FALSE; } gboolean is_string_in_array(GArray* array, const char* value) { for (unsigned i = 0; i < array->len; ++i) { char* item = g_array_index(array, char*, i); if (!g_strcmp0(value, item)) { return TRUE; } } return FALSE; } /* Check if the authentication key management algorithm uses a PSK password */ gboolean _is_auth_key_management_psk(const NetplanAuthenticationSettings* auth) { return ( auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK || auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_WPA_SAE); } gboolean _is_macaddress_special_nm_option(const char* value) { return ( !g_strcmp0(value, "preserve") || !g_strcmp0(value, "permanent") || !g_strcmp0(value, "random") || !g_strcmp0(value, "stable")); } gboolean _is_macaddress_special_nd_option(const char* value) { return ( !g_strcmp0(value, "permanent") || !g_strcmp0(value, "random")); } gboolean _is_valid_macaddress(const char* value) { static regex_t re; static gboolean re_inited = FALSE; if (!re_inited) { g_assert(regcomp(&re, "^[[:xdigit:]][[:xdigit:]](:[[:xdigit:]][[:xdigit:]]){5}((:[[:xdigit:]][[:xdigit:]]){14})?$", REG_EXTENDED|REG_NOSUB) == 0); re_inited = TRUE; } return regexec(&re, value, 0, NULL, 0) == 0; } netplan-1.0/src/validation.c000066400000000000000000000632451457004145200161140ustar00rootroot00000000000000/* * Copyright (C) 2019 Canonical, Ltd. * Author: Mathieu Trudel-Lapierre * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include "parse.h" #include "types-internal.h" #include "names.h" #include "error.h" #include "util-internal.h" #include "validation.h" /* Check coherence for address types */ gboolean is_ip4_address(const char* address) { struct in_addr a4; int ret; ret = inet_pton(AF_INET, address, &a4); g_assert(ret >= 0); if (ret > 0) return TRUE; return FALSE; } gboolean is_ip6_address(const char* address) { struct in6_addr a6; int ret; ret = inet_pton(AF_INET6, address, &a6); g_assert(ret >= 0); if (ret > 0) return TRUE; return FALSE; } gboolean is_hostname(const char *hostname) { static const gchar *pattern = "^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$"; return g_regex_match_simple(pattern, hostname, G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY); } gboolean is_wireguard_key(const char* key) { /* Check if this is (most likely) a 265bit, base64 encoded wireguard key */ if (strlen(key) == 44 && key[43] == '=' && key[42] != '=') { static const gchar *pattern = "^(?:[A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=)+$"; return g_regex_match_simple(pattern, key, 0, G_REGEX_MATCH_NOTEMPTY); } return FALSE; } /* Check coherence of OpenVSwitch controller targets */ gboolean validate_ovs_target(gboolean host_first, gchar* s) { static guint dport = 6653; // the default port g_autofree gchar* host = NULL; g_autofree gchar* port = NULL; gchar** vec = NULL; /* Format tcp:host[:port] or ssl:host[:port] */ if (host_first) { g_assert(s != NULL); // IP6 host, indicated by bracketed notation ([..IPv6..]) if (s[0] == '[') { gchar* tmp = NULL; tmp = s+1; //get rid of leading '[' // append default port to unify parsing if (!g_strrstr(tmp, "]:")) { gchar* host_port = g_strdup_printf("%s:%u", tmp, dport); vec = g_strsplit(host_port, "]:", 2); g_free(host_port); } else vec = g_strsplit(tmp, "]:", 2); // IP4 host } else { // append default port to unify parsing if (!g_strrstr(s, ":")) { gchar* host_port = g_strdup_printf("%s:%u", s, dport); vec = g_strsplit(host_port, ":", 2); g_free(host_port); } else vec = g_strsplit(s, ":", 2); } // host and port are always set host = g_strdup(vec[0]); //set host alias port = g_strdup(vec[1]); //set port alias g_assert(vec[2] == NULL); g_strfreev(vec); /* Format ptcp:[port][:host] or pssl:[port][:host] */ } else { // special case: "ptcp:" (no port, no host) if (!g_strcmp0(s, "")) port = g_strdup_printf("%u", dport); else { vec = g_strsplit(s, ":", 2); port = g_strdup(vec[0]); host = g_strdup(vec[1]); // get rid of leading & trailing IPv6 brackets if (host && host[0] == '[') { char **split = g_strsplit_set(host, "[]", 3); g_free(host); host = g_strjoinv("", split); g_strfreev(split); } g_strfreev(vec); } } g_assert(port != NULL); // special case where IPv6 notation contains '%iface' name if (host && g_strrstr(host, "%")) { gchar** split = g_strsplit (host, "%", 2); g_free(host); host = g_strdup(split[0]); // designated scope for IPv6 link-level addresses g_assert(split[1] != NULL && split[2] == NULL); g_strfreev(split); } if (atoi(port) > 0 && atoi(port) <= 65535) { if (!host) return TRUE; else if (host && (is_ip4_address(host) || is_ip6_address(host))) return TRUE; } return FALSE; } STATIC gboolean validate_interface_name_length(const NetplanNetDefinition* netdef) { gboolean validation = TRUE; char* iface = NULL; if (netdef->type >= NETPLAN_DEF_TYPE_VIRTUAL && netdef->type < NETPLAN_DEF_TYPE_NM) { if (strnlen(netdef->id, IF_NAMESIZE) == IF_NAMESIZE) { iface = netdef->id; validation = FALSE; } } else if (netdef->set_name) { if (strnlen(netdef->set_name, IF_NAMESIZE) == IF_NAMESIZE) { iface = netdef->set_name; validation = FALSE; } } /* TODO: make this a hard failure in the future, but keep it as a warning * for now, to not break netplan generate at boot. */ if (iface) g_warning("Interface name '%s' is too long. It will be ignored by the backend.", iface); return validation; } /************************************************ * Validation for grammar and backend rules. ************************************************/ STATIC gboolean validate_tunnel_key(const NetplanParser* npp, yaml_node_t* node, gchar* key, GError** error) { /* Tunnel key should be a number or dotted quad, except for wireguard. */ gchar* endptr; guint64 v = g_ascii_strtoull(key, &endptr, 10); if (*endptr != '\0' || v > G_MAXUINT) { /* Not a simple uint, try for a dotted quad */ if (!is_ip4_address(key)) return yaml_error(npp, node, error, "invalid tunnel key '%s'", key); } return TRUE; } STATIC gboolean validate_tunnel_grammar(const NetplanParser* npp, NetplanNetDefinition* nd, yaml_node_t* node, GError** error) { if (nd->tunnel.mode == NETPLAN_TUNNEL_MODE_UNKNOWN) return yaml_error(npp, node, error, "%s: missing or invalid 'mode' property for tunnel", nd->id); if (nd->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) { if (!nd->tunnel.private_key && nd->tunnel_private_key_flags == NETPLAN_KEY_FLAG_NONE) g_warning("%s: missing 'key' property (private key) for wireguard", nd->id); if (nd->tunnel.private_key && nd->tunnel.private_key[0] != '/' && !is_wireguard_key(nd->tunnel.private_key)) return yaml_error(npp, node, error, "%s: invalid wireguard private key", nd->id); if (!nd->wireguard_peers || nd->wireguard_peers->len == 0) { g_warning("%s: at least one peer is required.", nd->id); } else { for (guint i = 0; i < nd->wireguard_peers->len; i++) { NetplanWireguardPeer *peer = g_array_index (nd->wireguard_peers, NetplanWireguardPeer*, i); if (!peer->allowed_ips || peer->allowed_ips->len == 0) g_warning("%s: 'allowed-ips' is required for wireguard peers.", nd->id); if (peer->keepalive > 65535) return yaml_error(npp, node, error, "%s: keepalive must be 0-65535 inclusive.", nd->id); if (!peer->public_key) return yaml_error(npp, node, error, "%s: a public key is required.", nd->id); if (!is_wireguard_key(peer->public_key)) return yaml_error(npp, node, error, "%s: invalid wireguard public key", nd->id); if (peer->preshared_key && peer->preshared_key[0] != '/' && !is_wireguard_key(peer->preshared_key)) return yaml_error(npp, node, error, "%s: invalid wireguard shared key", nd->id); } } return TRUE; } else { if (nd->tunnel.input_key && !validate_tunnel_key(npp, node, nd->tunnel.input_key, error)) return FALSE; if (nd->tunnel.output_key && !validate_tunnel_key(npp, node, nd->tunnel.output_key, error)) return FALSE; } /* Validate local/remote IPs */ if (nd->tunnel.mode != NETPLAN_TUNNEL_MODE_VXLAN) { if (!nd->tunnel.remote_ip) return yaml_error(npp, node, error, "%s: missing 'remote' property for tunnel", nd->id); } if (nd->tunnel_ttl && nd->tunnel_ttl > 255) return yaml_error(npp, node, error, "%s: 'ttl' property for tunnel must be in range [1...255]", nd->id); switch(nd->tunnel.mode) { case NETPLAN_TUNNEL_MODE_IPIP6: case NETPLAN_TUNNEL_MODE_IP6IP6: case NETPLAN_TUNNEL_MODE_IP6GRE: case NETPLAN_TUNNEL_MODE_IP6GRETAP: case NETPLAN_TUNNEL_MODE_VTI6: if (nd->tunnel.local_ip && !is_ip6_address(nd->tunnel.local_ip)) return yaml_error(npp, node, error, "%s: 'local' must be a valid IPv6 address for this tunnel type", nd->id); if (!is_ip6_address(nd->tunnel.remote_ip)) return yaml_error(npp, node, error, "%s: 'remote' must be a valid IPv6 address for this tunnel type", nd->id); break; case NETPLAN_TUNNEL_MODE_VXLAN: if ((nd->tunnel.local_ip && nd->tunnel.remote_ip) && (is_ip6_address(nd->tunnel.local_ip) != is_ip6_address(nd->tunnel.remote_ip))) return yaml_error(npp, node, error, "%s: 'local' and 'remote' must be of same IP family type", nd->id); break; default: if (nd->tunnel.local_ip && !is_ip4_address(nd->tunnel.local_ip)) return yaml_error(npp, node, error, "%s: 'local' must be a valid IPv4 address for this tunnel type", nd->id); if (!is_ip4_address(nd->tunnel.remote_ip)) return yaml_error(npp, node, error, "%s: 'remote' must be a valid IPv4 address for this tunnel type", nd->id); break; } return TRUE; } STATIC gboolean validate_tunnel_backend_rules(const NetplanParser* npp, NetplanNetDefinition* nd, yaml_node_t* node, GError** error) { /* Backend-specific validation rules for tunnels */ switch (nd->backend) { case NETPLAN_BACKEND_NETWORKD: switch (nd->tunnel.mode) { case NETPLAN_TUNNEL_MODE_VTI: case NETPLAN_TUNNEL_MODE_VTI6: case NETPLAN_TUNNEL_MODE_WIREGUARD: case NETPLAN_TUNNEL_MODE_GRE: case NETPLAN_TUNNEL_MODE_IP6GRE: case NETPLAN_TUNNEL_MODE_GRETAP: case NETPLAN_TUNNEL_MODE_IP6GRETAP: break; /* TODO: Remove this exception and fix ISATAP handling with the * networkd backend. * systemd-networkd has grown ISATAP support in 918049a. */ case NETPLAN_TUNNEL_MODE_ISATAP: return yaml_error(npp, node, error, "%s: %s tunnel mode is not supported by networkd", nd->id, g_ascii_strup(netplan_tunnel_mode_name(nd->tunnel.mode), -1)); break; default: if (nd->tunnel.input_key) return yaml_error(npp, node, error, "%s: 'input-key' is not required for this tunnel type", nd->id); if (nd->tunnel.output_key) return yaml_error(npp, node, error, "%s: 'output-key' is not required for this tunnel type", nd->id); break; } break; case NETPLAN_BACKEND_NM: switch (nd->tunnel.mode) { case NETPLAN_TUNNEL_MODE_GRE: case NETPLAN_TUNNEL_MODE_IP6GRE: case NETPLAN_TUNNEL_MODE_WIREGUARD: case NETPLAN_TUNNEL_MODE_GRETAP: case NETPLAN_TUNNEL_MODE_IP6GRETAP: break; default: if (nd->tunnel.input_key) return yaml_error(npp, node, error, "%s: 'input-key' is not required for this tunnel type", nd->id); if (nd->tunnel.output_key) return yaml_error(npp, node, error, "%s: 'output-key' is not required for this tunnel type", nd->id); break; } break; default: break; //LCOV_EXCL_LINE } return TRUE; } gboolean validate_netdef_grammar(const NetplanParser* npp, NetplanNetDefinition* nd, GError** error) { int missing_id_count = g_hash_table_size(npp->missing_id); gboolean valid = FALSE; NetplanBackend backend = nd->backend; g_assert(nd->type != NETPLAN_DEF_TYPE_NONE); /* Skip all validation if we're missing some definition IDs (devices). * The ones we have yet to see may be necessary for validation to succeed, * we can complete it on the next parser pass. */ if (missing_id_count > 0) return TRUE; /* set-name: requires match: */ if (nd->set_name && !nd->has_match) return yaml_error(npp, NULL, error, "%s: 'set-name:' requires 'match:' properties", nd->id); if (nd->type == NETPLAN_DEF_TYPE_WIFI && nd->access_points == NULL) return yaml_error(npp, NULL, error, "%s: No access points defined", nd->id); if (nd->type == NETPLAN_DEF_TYPE_VLAN) { if (!nd->vlan_link) return yaml_error(npp, NULL, error, "%s: missing 'link' property", nd->id); nd->vlan_link->has_vlans = TRUE; if (nd->vlan_id == G_MAXUINT) return yaml_error(npp, NULL, error, "%s: missing 'id' property", nd->id); if (nd->vlan_id > 4094) return yaml_error(npp, NULL, error, "%s: invalid id '%u' (allowed values are 0 to 4094)", nd->id, nd->vlan_id); } if (nd->type == NETPLAN_DEF_TYPE_TUNNEL && nd->tunnel.mode == NETPLAN_TUNNEL_MODE_VXLAN) { if (nd->vxlan->vni == 0) return yaml_error(npp, NULL, error, "%s: missing 'id' property (VXLAN VNI)", nd->id); if (nd->vxlan->vni < 1 || nd->vxlan->vni > 16777215) return yaml_error(npp, NULL, error, "%s: VXLAN 'id' (VNI) " "must be in range [1..16777215]", nd->id); if (nd->vxlan->flow_label != G_MAXUINT && nd->vxlan->flow_label > 1048575) return yaml_error(npp, NULL, error, "%s: VXLAN 'flow-label' " "must be in range [0..1048575]", nd->id); } if (nd->type == NETPLAN_DEF_TYPE_VRF) { if (nd->vrf_table == G_MAXUINT) return yaml_error(npp, NULL, error, "%s: missing 'table' property", nd->id); } if (nd->type == NETPLAN_DEF_TYPE_TUNNEL) { valid = validate_tunnel_grammar(npp, nd, NULL, error); if (!valid) goto netdef_grammar_error; } if (nd->type == NETPLAN_DEF_TYPE_VETH) { if (!nd->veth_peer_link) return yaml_error(npp, NULL, error, "%s: virtual-ethernet missing 'peer' property", nd->id); } if (nd->ip6_addr_gen_mode != NETPLAN_ADDRGEN_DEFAULT && nd->ip6_addr_gen_token) return yaml_error(npp, NULL, error, "%s: ipv6-address-generation and ipv6-address-token are mutually exclusive", nd->id); if (nd->backend == NETPLAN_BACKEND_OVS) { // LCOV_EXCL_START if (!g_file_test(OPENVSWITCH_OVS_VSCTL, G_FILE_TEST_EXISTS)) { /* Tested via integration test */ return yaml_error(npp, NULL, error, "%s: The 'ovs-vsctl' tool is required to setup OpenVSwitch interfaces.", nd->id); } // LCOV_EXCL_STOP } if (nd->type == NETPLAN_DEF_TYPE_NM && (!nd->backend_settings.passthrough || !g_datalist_get_data(&nd->backend_settings.passthrough, "connection.type"))) return yaml_error(npp, NULL, error, "%s: network type 'nm-devices:' needs to provide a 'connection.type' via passthrough", nd->id); if (npp->current.netdef) validate_interface_name_length(npp->current.netdef); if (backend == NETPLAN_BACKEND_NONE) backend = npp->global_backend; if (nd->has_backend_settings_nm && backend != NETPLAN_BACKEND_NM) { return yaml_error(npp, NULL, error, "%s: networkmanager backend settings found but renderer is not NetworkManager.", nd->id); } valid = TRUE; netdef_grammar_error: return valid; } gboolean validate_backend_rules(const NetplanParser* npp, NetplanNetDefinition* nd, GError** error) { gboolean valid = FALSE; /* Set a placeholder, NULL yaml_node_t for error reporting */ yaml_node_t* node = NULL; g_assert(nd->type != NETPLAN_DEF_TYPE_NONE); if (nd->type == NETPLAN_DEF_TYPE_TUNNEL) { valid = validate_tunnel_backend_rules(npp, nd, node, error); if (!valid) goto backend_rules_error; } valid = TRUE; backend_rules_error: return valid; } gboolean validate_sriov_rules(const NetplanParser* npp, NetplanNetDefinition* nd, GError** error) { /* The SR-IOV checks need to be executed after all netdefs have been parsed; * only then can we calculate the PF/VF dependencies between the different * network definitions. */ NetplanNetDefinition* def; GHashTableIter iter; gboolean valid = FALSE; /* Set a placeholder, NULL yaml_node_t for error reporting */ yaml_node_t* node = NULL; g_assert(nd->type != NETPLAN_DEF_TYPE_NONE); if (nd->type == NETPLAN_DEF_TYPE_ETHERNET) { /* Is it defined as SR-IOV PF, explicitly? */ gboolean is_sriov_pf = nd->sriov_explicit_vf_count < G_MAXUINT; /* Does it have any VF pointing to it? (to mark it a PF implicitly) */ if (!is_sriov_pf) { g_hash_table_iter_init(&iter, npp->parsed_defs); while (g_hash_table_iter_next(&iter, NULL, (gpointer) &def)) { if (def->sriov_link == nd) { is_sriov_pf = TRUE; break; } } } gboolean eswitch_mode = (nd->embedded_switch_mode || nd->sriov_delay_virtual_functions_rebind); if (eswitch_mode && !is_sriov_pf) { valid = yaml_error(npp, node, error, "%s: This is not a SR-IOV PF", nd->id); goto sriov_rules_error; } } valid = TRUE; sriov_rules_error: return valid; } gboolean adopt_and_validate_vrf_routes(__unused const NetplanParser *npp, GHashTable *netdefs, GError **error) { gpointer key, value; GHashTableIter iter; g_hash_table_iter_init (&iter, netdefs); while (g_hash_table_iter_next (&iter, &key, &value)) { NetplanNetDefinition *nd = value; if (nd->type != NETPLAN_DEF_TYPE_VRF) continue; /* Routes */ if (nd->routes) { for (size_t i = 0; i < nd->routes->len; i++) { NetplanIPRoute* r = g_array_index(nd->routes, NetplanIPRoute*, i); if (r->table == nd->vrf_table) { g_debug("%s: Ignoring redundant routes table %d (matches VRF table)", nd->id, r->table); continue; } else if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC) { g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "%s: VRF routes table mismatch (%d != %d)", nd->id, nd->vrf_table, r->table); return FALSE; } else { r->table = nd->vrf_table; g_debug("%s: Adopted VRF routes table to %d", nd->id, nd->vrf_table); } } } /* IP Rules */ if (nd->ip_rules) { for (size_t i = 0; i < nd->ip_rules->len; i++) { NetplanIPRule* r = g_array_index(nd->ip_rules, NetplanIPRule*, i); if (r->table == nd->vrf_table) { g_debug("%s: Ignoring redundant routing-policy table %d (matches VRF table)", nd->id, r->table); continue; } else if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC && r->table != nd->vrf_table) { g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "%s: VRF routing-policy table mismatch (%d != %d)", nd->id, nd->vrf_table, r->table); return FALSE; } else { r->table = nd->vrf_table; g_debug("%s: Adopted VRF routing-policy table to %d", nd->id, nd->vrf_table); } } } } return TRUE; } struct _defroute_entry { gint family; guint table; guint metric; const char *netdef_id; }; STATIC void defroute_err(struct _defroute_entry *entry, const char *new_netdef_id, GError **error) { char table_name[128] = {}; char metric_name[128] = {}; g_assert(entry->family == AF_INET || entry->family == AF_INET6); // XXX: handle 254 as an alias for main ? if (entry->table == NETPLAN_ROUTE_TABLE_UNSPEC) strncpy(table_name, "table: main", sizeof(table_name) - 1); else snprintf(table_name, sizeof(table_name) - 1, "table: %d", entry->table); if (entry->metric == NETPLAN_METRIC_UNSPEC) strncpy(metric_name, "metric: default", sizeof(metric_name) - 1); else snprintf(metric_name, sizeof(metric_name) - 1, "metric: %u", entry->metric); g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "Conflicting default route declarations for %s (%s, %s), first declared in %s but also in %s", (entry->family == AF_INET) ? "IPv4" : "IPv6", table_name, metric_name, entry->netdef_id, new_netdef_id); } STATIC gboolean check_defroute(struct _defroute_entry *candidate, GSList **entries, GError **error) { struct _defroute_entry *entry; GSList *it; g_assert(entries != NULL); it = *entries; while (it) { struct _defroute_entry *e = it->data; if (e->family == candidate->family && e->table == candidate->table && e->metric == candidate->metric) { defroute_err(e, candidate->netdef_id, error); return FALSE; } it = it->next; } entry = g_malloc(sizeof(*entry)); *entry = *candidate; *entries = g_slist_prepend(*entries, entry); return TRUE; } gboolean validate_default_route_consistency(__unused const NetplanParser* npp, GHashTable *netdefs, GError ** error) { struct _defroute_entry candidate = {}; GSList *defroutes = NULL; gboolean ret = TRUE; gpointer key, value; GHashTableIter iter; g_hash_table_iter_init (&iter, netdefs); while (g_hash_table_iter_next (&iter, &key, &value)) { NetplanNetDefinition *nd = value; candidate.netdef_id = key; candidate.metric = NETPLAN_METRIC_UNSPEC; candidate.table = NETPLAN_ROUTE_TABLE_UNSPEC; if (nd->gateway4) { candidate.family = AF_INET; if (!check_defroute(&candidate, &defroutes, error)) { ret = FALSE; break; } } if (nd->gateway6) { candidate.family = AF_INET6; if (!check_defroute(&candidate, &defroutes, error)) { ret = FALSE; break; } } if (!nd->routes) continue; for (size_t i = 0; i < nd->routes->len; i++) { NetplanIPRoute* r = g_array_index(nd->routes, NetplanIPRoute*, i); char *suffix = strrchr(r->to, '/'); if (g_strcmp0(suffix, "/0") == 0 || g_strcmp0(r->to, "default") == 0) { candidate.family = r->family; candidate.table = r->table; candidate.metric = r->metric; if (!check_defroute(&candidate, &defroutes, error)) { ret = FALSE; break; } } } } g_slist_free_full(defroutes, g_free); return ret; } gboolean validate_veth_pair(__unused const NetplanState* np_state, const NetplanNetDefinition* netdef, GError** error) { NetplanNetDefinition* veth_peer = netdef->veth_peer_link; /* If the veth's peer type is the placeholder, it wasn't defined yet so it's not a non-veth device */ if (veth_peer && veth_peer->type != NETPLAN_DEF_TYPE_NM_PLACEHOLDER_) { if (veth_peer->type != NETPLAN_DEF_TYPE_VETH) { g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "%s: virtual-ethernet peer '%s' is not a virtual-ethernet interface\n", netdef->id, veth_peer->id); return FALSE; } /* If the veth's peer has a peer link and its type is the placeholder, it's because it's not * referring to the correct interface as its peer. * Example: A.peer = B, B.peer = C and C is a placeholder. */ if (veth_peer->veth_peer_link && veth_peer->veth_peer_link->type == NETPLAN_DEF_TYPE_NM_PLACEHOLDER_) { g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "%s: virtual-ethernet peer '%s' does not have a peer itself\n", netdef->id, veth_peer->id); return FALSE; } if (veth_peer->veth_peer_link && veth_peer->veth_peer_link != netdef) { g_set_error(error, NETPLAN_VALIDATION_ERROR, NETPLAN_ERROR_CONFIG_GENERIC, "%s: virtual-ethernet peer '%s' is another virtual-ethernet's (%s) peer already\n", netdef->id, veth_peer->id, veth_peer->veth_peer_link->id); return FALSE; } } return TRUE; } netplan-1.0/src/validation.h000066400000000000000000000031661457004145200161150ustar00rootroot00000000000000/* * Copyright (C) 2019 Canonical, Ltd. * Author: Mathieu Trudel-Lapierre * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "parse.h" #include #include gboolean is_ip4_address(const char* address); gboolean is_ip6_address(const char* address); gboolean is_hostname(const char* hostname); gboolean validate_ovs_target(gboolean host_first, gchar* s); gboolean is_wireguard_key(const char* hostname); gboolean validate_netdef_grammar(const NetplanParser* npp, NetplanNetDefinition* nd, GError** error); gboolean validate_backend_rules(const NetplanParser* npp, NetplanNetDefinition* nd, GError** error); gboolean validate_sriov_rules(const NetplanParser* npp, NetplanNetDefinition* nd, GError** error); gboolean validate_default_route_consistency(const NetplanParser* npp, GHashTable* netdefs, GError** error); gboolean adopt_and_validate_vrf_routes(const NetplanParser* npp, GHashTable* netdefs, GError** error); gboolean validate_veth_pair(const NetplanState* np_state, const NetplanNetDefinition* netdef, GError** error); netplan-1.0/src/yaml-helpers.h000066400000000000000000000105651457004145200163660ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * Author: Martin Pitt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #define YAML_MAPPING_OPEN(event_ptr, emitter_ptr) \ { \ yaml_mapping_start_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_MAP_TAG, 1, YAML_BLOCK_MAPPING_STYLE); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ } #define YAML_MAPPING_CLOSE(event_ptr, emitter_ptr) \ { \ yaml_mapping_end_event_initialize(event_ptr); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ } #define YAML_SEQUENCE_OPEN(event_ptr, emitter_ptr) \ { \ yaml_sequence_start_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_SEQ_TAG, 1, YAML_BLOCK_SEQUENCE_STYLE); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ } #define YAML_SEQUENCE_CLOSE(event_ptr, emitter_ptr) \ { \ yaml_sequence_end_event_initialize(event_ptr); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ } #define YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, scalar) \ { \ yaml_scalar_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_STR_TAG, (yaml_char_t *)scalar, strlen(scalar), 1, 0, YAML_PLAIN_SCALAR_STYLE); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ } #define YAML_FLAG(event_ptr, emitter_ptr, flag, flags_ptr, flags_func) \ if (flags_ptr & flag) \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, flags_func(flag)); #define YAML_NULL_PLAIN(event_ptr, emitter_ptr) \ yaml_scalar_event_initialize(event_ptr, NULL, (yaml_char_t*)YAML_NULL_TAG, (yaml_char_t*)"null", strlen("null"), 1, 0, YAML_PLAIN_SCALAR_STYLE); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ /* Implicit plain and quoted tags, double quoted style */ #define YAML_SCALAR_QUOTED(event_ptr, emitter_ptr, scalar) \ { \ yaml_scalar_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_STR_TAG, (yaml_char_t *)scalar, strlen(scalar), 1, 1, YAML_DOUBLE_QUOTED_SCALAR_STYLE); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ } #define YAML_NONNULL_STRING(event_ptr, emitter_ptr, key, value_ptr) \ { \ if (value_ptr) { \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ YAML_SCALAR_QUOTED(event_ptr, emitter_ptr, value_ptr); \ } \ } #define YAML_NONNULL_STRING_PLAIN(event_ptr, emitter_ptr, key, value_ptr) \ { \ if (value_ptr) { \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, value_ptr); \ } \ } #define _YAML_UINT(event_ptr, emitter_ptr, key, value) \ { \ tmp = g_strdup_printf("%u", value); \ YAML_NONNULL_STRING_PLAIN(event_ptr, emitter_ptr, key, tmp); \ g_free(tmp); \ } #define YAML_NULL(event_ptr, emitter_ptr, key) \ YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ YAML_NULL_PLAIN(event_ptr, emitter_ptr); \ /* open YAML emitter, document, stream and initial mapping */ #define YAML_OUT_START(event_ptr, emitter_ptr, file) \ { \ yaml_emitter_initialize(emitter_ptr); \ yaml_emitter_set_output_file(emitter_ptr, file); \ yaml_stream_start_event_initialize(event_ptr, YAML_UTF8_ENCODING); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ yaml_document_start_event_initialize(event_ptr, NULL, NULL, NULL, 1); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ YAML_MAPPING_OPEN(event_ptr, emitter_ptr); \ } /* close initial YAML mapping, document, stream and emitter */ #define YAML_OUT_STOP(event_ptr, emitter_ptr) \ { \ YAML_MAPPING_CLOSE(event_ptr, emitter_ptr); \ yaml_document_end_event_initialize(event_ptr, 1); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ yaml_stream_end_event_initialize(event_ptr); \ if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto err_path; \ yaml_emitter_delete(emitter_ptr); \ } netplan-1.0/tests/000077500000000000000000000000001457004145200141575ustar00rootroot00000000000000netplan-1.0/tests/cli/000077500000000000000000000000001457004145200147265ustar00rootroot00000000000000netplan-1.0/tests/cli/__init__.py000066400000000000000000000000001457004145200170250ustar00rootroot00000000000000netplan-1.0/tests/cli/test_get_set.py000066400000000000000000000756001457004145200200010ustar00rootroot00000000000000#!/usr/bin/python3 # Functional tests of netplan CLI. These are run during "make check" and don't # touch the system configuration at all. # # Copyright (C) 2020 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import unittest import tempfile import shutil import glob import yaml from netplan_cli.cli.commands.set import FALLBACK_FILENAME from netplan_cli.cli.ovs import OPENVSWITCH_OVS_VSCTL from netplan import NetplanException from tests.test_utils import call_cli class TestSet(unittest.TestCase): '''Test netplan set''' def setUp(self): self.workdir = tempfile.TemporaryDirectory(prefix='netplan_') self.file = '70-netplan-set.yaml' self.path = os.path.join(self.workdir.name, 'etc', 'netplan', self.file) os.makedirs(os.path.join(self.workdir.name, 'etc', 'netplan')) def tearDown(self): shutil.rmtree(self.workdir.name) def _set(self, args): args.insert(0, 'set') out = call_cli(args + ['--root-dir', self.workdir.name]) self.assertEqual(out, '', msg='netplan set returned unexpected output') def test_set_scalar(self): self._set(['ethernets.eth0.dhcp4=true']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: self.assertIs(True, yaml.safe_load(f)['network']['ethernets']['eth0']['dhcp4']) def test_set_scalar2(self): self._set(['ethernets.eth0.dhcp4="yes"']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: # XXX: the previous version using PyYAML would keep the "yes" variant but the round-trip # through libnetplan doesn't keep formatting choices (yes is a keyword same as true) self.assertIs(True, yaml.safe_load(f)['network']['ethernets']['eth0']['dhcp4']) def test_set_global(self): self._set([r'network={renderer: NetworkManager}']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: self.assertEqual('NetworkManager', yaml.safe_load(f)['network']['renderer']) def test_set_sequence(self): self._set(['ethernets.eth0.addresses=[1.2.3.4/24, \'5.6.7.8/24\']']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: self.assertListEqual( ['1.2.3.4/24', '5.6.7.8/24'], yaml.safe_load(f)['network']['ethernets']['eth0']['addresses']) def test_set_sequence2(self): self._set(['ethernets.eth0.addresses=["1.2.3.4/24", 5.6.7.8/24]']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: self.assertListEqual( ['1.2.3.4/24', '5.6.7.8/24'], yaml.safe_load(f)['network']['ethernets']['eth0']['addresses']) def test_set_mapping(self): self._set(['ethernets.eth0={addresses: [1.2.3.4/24], dhcp4: true}']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertSequenceEqual( ['1.2.3.4/24'], out['network']['ethernets']['eth0']['addresses']) self.assertIs(True, out['network']['ethernets']['eth0']['dhcp4']) def test_set_origin_hint(self): self._set(['ethernets.eth0.dhcp4=true', '--origin-hint=99_snapd']) p = os.path.join(self.workdir.name, 'etc', 'netplan', '99_snapd.yaml') self.assertTrue(os.path.isfile(p)) with open(p, 'r') as f: self.assertIs(True, yaml.safe_load(f)['network']['ethernets']['eth0']['dhcp4']) def test_set_origin_hint_update(self): hint = os.path.join(self.workdir.name, 'etc', 'netplan', 'hint.yaml') with open(hint, 'w') as f: f.write('''network: version: 2 renderer: networkd ethernets: {eth0: {dhcp6: true}}''') self._set(['ethernets.eth0={dhcp4: true, dhcp6: NULL}', '--origin-hint=hint']) self.assertTrue(os.path.isfile(hint)) with open(hint, 'r') as f: yml = yaml.safe_load(f) self.assertEqual(2, yml['network']['version']) self.assertEqual('networkd', yml['network']['renderer']) self.assertTrue(yml['network']['ethernets']['eth0']['dhcp4']) self.assertNotIn('dhcp6', yml['network']['ethernets']['eth0']) def test_set_origin_hint_override(self): defaults = os.path.join(self.workdir.name, 'etc', 'netplan', '0-snapd-defaults.yaml') with open(defaults, 'w') as f: f.write('''network: renderer: networkd bridges: {br54: {dhcp4: true, dhcp6: true}} ethernets: {eth0: {dhcp4: true}}''') self._set(['network.version=2', '--origin-hint=90-snapd-config']) self._set(['renderer=NetworkManager', '--origin-hint=90-snapd-config']) self._set(['bridges.br55.dhcp4=false', '--origin-hint=90-snapd-config']) self._set(['bridges.br54.dhcp4=false', '--origin-hint=90-snapd-config']) self._set(['bridges.br54.interfaces=[eth0]', '--origin-hint=90-snapd-config']) self.assertTrue(os.path.isfile(defaults)) with open(defaults, 'r') as f: yml = yaml.safe_load(f) self.assertEqual("networkd", yml['network']['renderer']) p = os.path.join(self.workdir.name, 'etc', 'netplan', '90-snapd-config.yaml') self.assertTrue(os.path.isfile(p)) with open(p, 'r') as f: yml = yaml.safe_load(f) self.assertIs(False, yml['network']['bridges']['br54']['dhcp4']) self.assertNotIn('dhcp6', yml['network']['bridges']['br54']) self.assertEqual(['eth0'], yml['network']['bridges']['br54']['interfaces']) self.assertIs(False, yml['network']['bridges']['br55']['dhcp4']) self.assertIs(2, yml['network']['version']) self.assertEqual("NetworkManager", yml['network']['renderer']) def test_set_origin_hint_override_no_leak_renderer(self): defaults = os.path.join(self.workdir.name, 'etc', 'netplan', '0-snapd-defaults.yaml') with open(defaults, 'w') as f: f.write('''network: renderer: networkd bridges: {br54: {dhcp4: true}}''') self._set(['bridges.br54.dhcp4=false', '--origin-hint=90-snapd-config']) self.assertTrue(os.path.isfile(defaults)) with open(defaults, 'r') as f: yml = yaml.safe_load(f) self.assertEqual("networkd", yml['network']['renderer']) p = os.path.join(self.workdir.name, 'etc', 'netplan', '90-snapd-config.yaml') self.assertTrue(os.path.isfile(p)) with open(p, 'r') as f: yml = yaml.safe_load(f) self.assertIs(False, yml['network']['bridges']['br54']['dhcp4']) self.assertNotIn('renderer', yml['network']) def test_set_origin_hint_override_invalid_netdef_setting(self): defaults = os.path.join(self.workdir.name, 'etc', 'netplan', '0-snapd-defaults.yaml') with open(defaults, 'w') as f: f.write('''network: vrfs: vrf0: table: 1005 routes: - to: default via: 10.10.10.4 table: 1005 ''') with self.assertRaises(NetplanException) as e: self._set(['vrfs.vrf0.table=1004', '--origin-hint=90-snapd-config']) self.assertIn('vrf0: VRF routes table mismatch (1004 != 1005)', str(e.exception)) # hint/output file should not exist p = os.path.join(self.workdir.name, 'etc', 'netplan', '90-snapd-config.yaml') self.assertFalse(os.path.isfile(p)) # original (defaults) file should stay untouched self.assertTrue(os.path.isfile(defaults)) with open(defaults, 'r') as f: yml = yaml.safe_load(f) self.assertEqual(1005, yml['network']['vrfs']['vrf0']['table']) def test_set_origin_hint_extend(self): p = os.path.join(self.workdir.name, 'etc', 'netplan', '90-snapd-config.yaml') with open(p, 'w') as f: f.write('''network: {bridges: {br54: {dhcp4: true}}}''') self._set(['bridges.br54.dhcp4=false', '--origin-hint=90-snapd-config']) self._set(['bridges.br55.dhcp4=true', '--origin-hint=90-snapd-config']) self.assertTrue(os.path.isfile(p)) with open(p, 'r') as f: yml = yaml.safe_load(f) self.assertIs(False, yml['network']['bridges']['br54']['dhcp4']) self.assertIs(True, yml['network']['bridges']['br55']['dhcp4']) self.assertNotIn('renderer', yml['network']) def test_set_empty_origin_hint(self): with self.assertRaises(Exception) as context: self._set(['ethernets.eth0.dhcp4=true', '--origin-hint=']) self.assertTrue('Invalid/empty origin-hint' in str(context.exception)) def test_set_empty_hint_file(self): empty_file = os.path.join(self.workdir.name, 'etc', 'netplan', '00-empty.yaml') open(empty_file, 'w').close() # touch 00-empty.yaml self._set(['ethernets.eth0.dhcp4=true', '--origin-hint=00-empty']) self.assertTrue(os.path.isfile(empty_file)) with open(empty_file, 'r') as f: self.assertTrue(yaml.safe_load(f)['network']['ethernets']['eth0']['dhcp4']) def test_set_empty_hint_file_whitespace(self): empty_file = os.path.join(self.workdir.name, 'etc', 'netplan', '00-empty.yaml') with open(empty_file, 'w') as f: f.write('\n') # echo "" > 00-empty.yaml self._set(['ethernets.eth0=null', '--origin-hint=00-empty']) self.assertFalse(os.path.isfile(empty_file)) def test_set_network_null_hint(self): not_a_file = os.path.join(self.workdir.name, 'etc', 'netplan', '00-no-exist.yaml') self._set(['network=null', '--origin-hint=00-no-exist']) self.assertFalse(os.path.isfile(not_a_file)) def test_unset_non_existing_hint(self): not_a_file = os.path.join(self.workdir.name, 'etc', 'netplan', '00-no-exist.yaml') self._set(['network.ethernets=null', '--origin-hint=00-no-exist']) self.assertFalse(os.path.isfile(not_a_file)) def test_set_network_null_hint_rm(self): some_hint = os.path.join(self.workdir.name, 'etc', 'netplan', '00-some-hint.yaml') with open(some_hint, 'w') as f: f.write('network: {ethernets: {eth0: {dhcp4: true}}}') with open(self.path, 'w') as f: f.write('network: {version: 2}') self._set(['network=null', '--origin-hint=00-some-hint']) self.assertFalse(os.path.isfile(some_hint)) # the hint file is deleted self.assertTrue(os.path.isfile(self.path)) # any other YAML still exists def test_set_network_null_global(self): some_hint = os.path.join(self.workdir.name, 'etc', 'netplan', '00-some-hint.yaml') with open(some_hint, 'w') as f: f.write('network: {ethernets: {eth0: {dhcp4: true}}}') with open(self.path, 'w') as f: f.write('network: {version: 2}') self._set(['network=null']) any_yaml = glob.glob(os.path.join(self.workdir.name, 'etc', 'netplan', '*.yaml')) self.assertEqual(any_yaml, []) self.assertFalse(os.path.isfile(self.path)) self.assertFalse(os.path.isfile(some_hint)) def test_set_no_netdefs_just_globals(self): # LP: #2027584 keepme1 = os.path.join(self.workdir.name, 'etc', 'netplan', '00-no-netdefs-just-renderer.yaml') with open(keepme1, 'w') as f: f.write('''network: {renderer: NetworkManager}''') keepme2 = os.path.join(self.workdir.name, 'etc', 'netplan', '00-no-netdefs-just-version.yaml') with open(keepme2, 'w') as f: f.write('''network: {version: 2}''') deleteme = os.path.join(self.workdir.name, 'etc', 'netplan', '90-some-netdefs.yaml') with open(deleteme, 'w') as f: f.write('''network: {ethernets: {eth99: {dhcp4: true}}}''') self._set(['ethernets.eth99=NULL']) self.assertFalse(os.path.isfile(deleteme)) self.assertTrue(os.path.isfile(keepme1)) with open(keepme1, 'r') as f: yml = yaml.safe_load(f) self.assertEqual('NetworkManager', yml['network']['renderer']) # XXX: It's probably fine to delete a file that just contains "version: 2" # Or is it? And what about other globals, such as OVS ports? self.assertFalse(os.path.isfile(keepme2)) def test_set_clear_netdefs_keep_globals(self): # LP: #2027584 keep = os.path.join(self.workdir.name, 'etc', 'netplan', '00-keep.yaml') with open(keep, 'w') as f: f.write('''network: version: 2 renderer: NetworkManager bridges: br54: addresses: [1.2.3.4/24] ''') self._set(['network.bridges.br54=NULL']) self.assertTrue(os.path.isfile(keep)) with open(keep, 'r') as f: yml = yaml.safe_load(f) self.assertEqual(2, yml['network']['version']) self.assertEqual('NetworkManager', yml['network']['renderer']) self.assertNotIn('bridges', yml['network']) default = os.path.join(self.workdir.name, 'etc', 'netplan', FALLBACK_FILENAME) self.assertFalse(os.path.isfile(default)) def test_set_clear_netdefs_keep_globals_default_renderer(self): keep = os.path.join(self.workdir.name, 'etc', 'netplan', '00-keep.yaml') with open(keep, 'w') as f: f.write('''network: version: 2 renderer: NetworkManager bridges: br54: addresses: [1.2.3.4/24] ''') default = os.path.join(self.workdir.name, 'etc', 'netplan', FALLBACK_FILENAME) with open(default, 'w') as f: f.write('''network: renderer: networkd ''') self._set(['network.bridges.br54=NULL']) self.assertTrue(os.path.isfile(keep)) with open(keep, 'r') as f: yml = yaml.safe_load(f) self.assertEqual(2, yml['network']['version']) self.assertEqual('NetworkManager', yml['network']['renderer']) self.assertNotIn('bridges', yml['network']) self.assertTrue(os.path.isfile(default)) with open(default, 'r') as f: yml = yaml.safe_load(f) self.assertEqual(2, yml['network']['version']) self.assertEqual('networkd', yml['network']['renderer']) def test_set_invalid(self): with self.assertRaises(Exception) as context: self._set(['xxx.yyy=abc']) self.assertIn('unknown key \'xxx\'', str(context.exception)) self.assertFalse(os.path.isfile(self.path)) def test_set_invalid_validation(self): with self.assertRaises(Exception) as context: self._set(['ethernets.eth0.set-name=myif0']) self.assertIn('eth0: \'set-name:\' requires \'match:\' properties', str(context.exception)) self.assertFalse(os.path.isfile(self.path)) def test_set_invalid_validation2(self): with open(self.path, 'w') as f: f.write('''network: tunnels: tun0: mode: sit local: 1.2.3.4 remote: 5.6.7.8''') with self.assertRaises(NetplanException) as context: self._set(['tunnels.tun0.keys.input=12345']) self.assertIn('tun0: \'input-key\' is not required for this tunnel type', str(context.exception)) def test_set_append(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: ens3: {dhcp4: yes}''') self._set(['ethernets.eth0.dhcp4=true']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertIn('ens3', out['network']['ethernets']) self.assertIn('eth0', out['network']['ethernets']) self.assertIs(True, out['network']['ethernets']['ens3']['dhcp4']) self.assertIs(True, out['network']['ethernets']['eth0']['dhcp4']) self.assertEqual(2, out['network']['version']) def test_set_overwrite_eq(self): with open(self.path, 'w') as f: f.write('''network: ethernets: ens3: {dhcp4: "yes"}''') self._set(['ethernets.ens3.dhcp4=yes']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: self.assertIs( True, yaml.safe_load(f)['network']['ethernets']['ens3']['dhcp4']) def test_set_overwrite(self): p = os.path.join(self.workdir.name, 'etc', 'netplan', 'test.yaml') with open(p, 'w') as f: f.write('''network: renderer: networkd ethernets: ens3: {dhcp4: "no"}''') self._set(['ethernets.ens3.dhcp4=true']) self.assertTrue(os.path.isfile(p)) with open(p, 'r') as f: yml = yaml.safe_load(f) self.assertIs(True, yml['network']['ethernets']['ens3']['dhcp4']) self.assertEqual('networkd', yml['network']['renderer']) def test_set_delete(self): with open(self.path, 'w') as f: f.write('''network:\n version: 2\n renderer: NetworkManager ethernets: ens3: {dhcp4: yes, dhcp6: yes} eth0: {addresses: [1.2.3.4/24]} eth1: {addresses: [2.3.4.5/24]} ''') self._set(['ethernets.eth0.addresses=NULL']) self._set(['ethernets.ens3.dhcp6=null']) self._set(['ethernets.eth1=NULL']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertIn('ethernets', out['network']) self.assertEqual(2, out['network']['version']) self.assertIs(True, out['network']['ethernets']['ens3']['dhcp4']) self.assertNotIn('dhcp6', out['network']['ethernets']['ens3']) self.assertNotIn('eth0', out['network']['ethernets']) self.assertNotIn('eth1', out['network']['ethernets']) def test_set_delete_subtree(self): with open(self.path, 'w') as f: f.write('''network:\n version: 2\n renderer: NetworkManager ethernets: eth0: {addresses: [1.2.3.4/24]}''') self._set(['network.ethernets=null']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertIn('network', out) self.assertEqual(2, out['network']['version']) self.assertEqual('NetworkManager', out['network']['renderer']) self.assertNotIn('ethernets', out['network']) def test_set_global_ovs(self): with open(self.path, 'w') as f: f.write('''network:\n version: 2 ethernets: eth0: {addresses: [1.2.3.4/24]}''') self._set(['network.openvswitch={"ports": [[port1, port2]], "other-config": null}']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertIn('network', out) self.assertEqual(2, out['network']['version']) self.assertEqual('1.2.3.4/24', out['network']['ethernets']['eth0']['addresses'][0]) self.assertNotIn('other-config', out['network']['openvswitch']) self.assertSequenceEqual(['port1', 'port2'], out['network']['openvswitch']['ports'][0]) def test_set_delete_access_point(self): with open(self.path, 'w') as f: f.write('''network: version: 2 wifis: wl0: access-points: "Joe's Home": password: "s0s3kr1t" bssid: 00:11:22:33:44:55 band: 2.4GHz channel: 11 workplace: password: "c0mpany1" bssid: de:ad:be:ef:ca:fe band: 5GHz channel: 100 peer2peer: mode: adhoc''') self._set(['network.wifis.wl0.access-points.Joe\'s Home=null']) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertNotIn('Joe\'s Home', out['network']['wifis']['wl0']['access-points']) def test_set_delete_nm_passthrough(self): with open(self.path, 'w') as f: f.write('''network: version: 2 wifis: wlan0: renderer: NetworkManager dhcp4: true macaddress: "00:11:22:33:44:55" access-points: "SOME-SSID": bssid: "de:ad:be:ef:ca:fe" networkmanager: name: "myid with spaces" passthrough: connection.permissions: "" ipv4.dns-search: ""''') ap_key = 'network.wifis.wlan0.access-points.SOME-SSID' self._set([ap_key+'.networkmanager.passthrough.connection\\.permissions=null']) with open(self.path, 'r') as f: out = yaml.safe_load(f) ap = out['network']['wifis']['wlan0']['access-points']['SOME-SSID'] self.assertNotIn('connection.permissions', ap['networkmanager']['passthrough']) self.assertEqual('', ap['networkmanager']['passthrough']['ipv4.dns-search']) def test_set_delete_bridge_subparams(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: eno1: {} eno2: {} switchports: match: driver: yayroute bridges: br0: interfaces: [eno1, eno2, switchports] parameters: path-cost: eno1: 70 eno2: 80 port-priority: eno1: 14 eno2: 15''') self._set(['network.bridges.br0.parameters={path-cost: {eno1: null}, port-priority: {eno2: null}}']) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertNotIn('eno1', out['network']['bridges']['br0']['parameters']['path-cost']) self.assertEqual(80, out['network']['bridges']['br0']['parameters']['path-cost']['eno2']) self.assertNotIn('eno2', out['network']['bridges']['br0']['parameters']['port-priority']) self.assertEqual(14, out['network']['bridges']['br0']['parameters']['port-priority']['eno1']) @unittest.skipIf(not os.path.exists(OPENVSWITCH_OVS_VSCTL), 'OpenVSwitch not installed') def test_set_delete_ovs_other_config(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: eth0: openvswitch: other-config: bogus-option: bogus disable-in-band: true dhcp6: true bridges: ovs0: interfaces: [eth0] openvswitch: {} ''') self._set(['ethernets.eth0.openvswitch.other-config.bogus-option=null']) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertNotIn('bogus-option', out['network']['ethernets']['eth0']['openvswitch']['other-config']) self.assertTrue(out['network']['ethernets']['eth0']['openvswitch']['other-config']['disable-in-band']) def test_set_delete_file(self): with open(self.path, 'w') as f: f.write('''network: ethernets: ens3: {dhcp4: yes}''') self._set(['network.ethernets.ens3.dhcp4=NULL']) # The file should be deleted if this was the last/only key left self.assertFalse(os.path.isfile(self.path)) def test_set_delete_file_with_version(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: ens3: {dhcp4: yes}''') self._set(['network.ethernets.ens3=NULL']) # The file should be deleted if only "network: {version: 2}" is left self.assertFalse(os.path.isfile(self.path)) def test_set_invalid_delete(self): with open(self.path, 'w') as f: f.write('''network:\n version: 2\n renderer: NetworkManager ethernets: eth0: {addresses: [1.2.3.4]}''') with self.assertRaises(Exception) as context: self._set(['ethernets.eth0.addresses']) self.assertEqual('Invalid value specified', str(context.exception)) def test_set_escaped_dot(self): self._set([r'ethernets.eth0\.123.dhcp4=false']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertIs(False, out['network']['ethernets']['eth0.123']['dhcp4']) def test_set_invalid_input(self): with self.assertRaises(Exception) as context: self._set([r'ethernets.eth0={dhcp4:false}']) self.assertIn( "unknown key 'dhcp4:false'", str(context.exception)) def test_set_override_existing_file(self): override = os.path.join(self.workdir.name, 'etc', 'netplan', 'some-file.yaml') with open(override, 'w') as f: f.write(r'network: {ethernets: {eth0: {dhcp4: true}, eth1: {dhcp6: false}}}') self._set([r'ethernets.eth0.dhcp4=false']) self.assertFalse(os.path.isfile(self.path)) self.assertTrue(os.path.isfile(override)) with open(override, 'r') as f: out = yaml.safe_load(f) self.assertIs(False, out['network']['ethernets']['eth0']['dhcp4']) self.assertIs(False, out['network']['ethernets']['eth1']['dhcp6']) def test_set_override_existing_file_escaped_dot(self): override = os.path.join(self.workdir.name, 'etc', 'netplan', 'some-file.yaml') with open(override, 'w') as f: f.write(r'network: {ethernets: {eth0.123: {dhcp4: true}}}') self._set([r'ethernets.eth0\.123.dhcp4=false']) self.assertFalse(os.path.isfile(self.path)) self.assertTrue(os.path.isfile(override)) with open(override, 'r') as f: out = yaml.safe_load(f) self.assertIs(False, out['network']['ethernets']['eth0.123']['dhcp4']) def test_set_override_multiple_existing_files(self): file1 = os.path.join(self.workdir.name, 'etc', 'netplan', 'eth0.yaml') with open(file1, 'w') as f: f.write(r'network: {ethernets: {eth0.1: {dhcp4: true}, eth0.2: {dhcp4: true}}}') file2 = os.path.join(self.workdir.name, 'etc', 'netplan', 'eth1.yaml') with open(file2, 'w') as f: f.write(r'network: {ethernets: {eth1: {dhcp4: true}}}') self._set([(r'network={"renderer": "NetworkManager", "version":2,' r'"ethernets":{' r'"eth1":{"dhcp4":false},' r'"eth0.1":{"dhcp4":false},' r'"eth0.2":{"dhcp4":false}},' r'"bridges":{' r'"br99":{"dhcp4":false}}}')]) self.assertTrue(os.path.isfile(file1)) with open(file1, 'r') as f: self.assertIs(False, yaml.safe_load(f)['network']['ethernets']['eth0.1']['dhcp4']) self.assertTrue(os.path.isfile(file2)) with open(file2, 'r') as f: self.assertIs(False, yaml.safe_load(f)['network']['ethernets']['eth1']['dhcp4']) self.assertTrue(os.path.isfile(self.path)) with open(self.path, 'r') as f: out = yaml.safe_load(f) self.assertIs(False, out['network']['bridges']['br99']['dhcp4']) self.assertEqual(2, out['network']['version']) self.assertEqual('NetworkManager', out['network']['renderer']) class TestGet(unittest.TestCase): '''Test netplan get''' def setUp(self): self.workdir = tempfile.TemporaryDirectory() self.file = '00-config.yaml' self.path = os.path.join(self.workdir.name, 'etc', 'netplan', self.file) os.makedirs(os.path.join(self.workdir.name, 'etc', 'netplan')) def _get(self, args): args.insert(0, 'get') return call_cli(args + ['--root-dir', self.workdir.name]) def test_get_scalar(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: ens3: {dhcp4: yes}''') out = self._get(['ethernets.ens3.dhcp4']) self.assertIn('true', out) def test_get_mapping(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: ens3: dhcp4: yes addresses: [1.2.3.4/24, 5.6.7.8/24]''') out = yaml.safe_load(self._get(['ethernets'])) self.assertDictEqual({'ens3': {'addresses': ['1.2.3.4/24', '5.6.7.8/24'], 'dhcp4': True}}, out) def test_get_modems(self): with open(self.path, 'w') as f: f.write('''network: version: 2 modems: wwan0: apn: internet pin: 1234 dhcp4: yes addresses: [1.2.3.4/24, 5.6.7.8/24]''') out = yaml.safe_load(self._get(['modems.wwan0'])) self.assertDictEqual({ 'addresses': ['1.2.3.4/24', '5.6.7.8/24'], 'apn': 'internet', 'dhcp4': True, 'pin': '1234' }, out) def test_get_sequence(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: ens3: {addresses: [1.2.3.4/24, 5.6.7.8/24]}''') out = yaml.safe_load(self._get(['network.ethernets.ens3.addresses'])) self.assertSequenceEqual(['1.2.3.4/24', '5.6.7.8/24'], out) def test_get_null(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: ens3: {dhcp4: yes}''') out = self._get(['ethernets.eth0.dhcp4']) self.assertEqual('null\n', out) def test_get_escaped_dot(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: eth0.123: {dhcp4: yes}''') out = self._get([r'ethernets.eth0\.123.dhcp4']) self.assertEqual('true\n', out) def test_get_all(self): with open(self.path, 'w') as f: f.write('''network: version: 2 ethernets: eth0: {dhcp4: yes}''') out = yaml.safe_load(self._get([])) self.assertDictEqual({'network': { 'ethernets': {'eth0': {'dhcp4': True}}, 'version': 2, } }, out) def test_get_network(self): with open(self.path, 'w') as f: f.write('network:\n version: 2\n renderer: NetworkManager') out = yaml.safe_load(self._get(['network'])) self.assertDictEqual({'renderer': 'NetworkManager', 'version': 2}, out) def test_get_bad_network(self): with open(self.path, 'w') as f: f.write('network:\n version: 2\n renderer: NetworkManager') out = yaml.safe_load(self._get(['networkINVALID'])) self.assertIsNone(out) def test_get_yaml_document_end_failure(self): with open(self.path, 'w') as f: f.write('''network: ethernets: eth0: match: name: "test" mtu: 9000 set-name: "yo" dhcp4: true virtual-function-count: 2 ''') # this shall not throw any (YAML DOCUMENT-END) exception out = yaml.safe_load(self._get(['ethernets.eth0'])) self.assertListEqual(['match', 'dhcp4', 'set-name', 'mtu', 'virtual-function-count'], list(out)) netplan-1.0/tests/cli/test_state.py000066400000000000000000000516711457004145200174710ustar00rootroot00000000000000#!/usr/bin/python3 # Closed-box tests of netplan CLI. These are run during "make check" and don't # touch the system configuration at all. # # Copyright (C) 2023 Canonical, Ltd. # Authors: Lukas Märdian # Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import copy import os import shutil import subprocess import tempfile import unittest import yaml from unittest.mock import patch, call, mock_open from netplan_cli.cli.state import Interface, NetplanConfigState, SystemConfigState from .test_status import (BRIDGE, DNS_ADDRESSES, DNS_IP4, DNS_SEARCH, FAKE_DEV, IPROUTE2, NETWORKD, NMCLI, ROUTE4, ROUTE6) class resolve1_ipc_mock(): def get_object(self, _foo, _bar): return {} # dbus Object class resolve1_iface_mock(): def __init__(self, _foo, _bar): pass # dbus Interface def GetAll(self, _): return { 'DNS': DNS_ADDRESSES, 'Domains': DNS_SEARCH, } class TestSystemState(unittest.TestCase): '''Test netplan state module''' def setUp(self): self.maxDiff = None @patch('subprocess.check_output') def test_query_iproute2(self, mock): mock.return_value = IPROUTE2 res = SystemConfigState.query_iproute2() mock.assert_called_with(['ip', '-d', '-j', 'addr'], text=True) self.assertEqual(len(res), 7) self.assertListEqual([itf.get('ifname') for itf in res], ['lo', 'enp0s31f6', 'wlan0', 'wg0', 'wwan0', 'tun0', 'tun1']) @patch('subprocess.check_output') def test_query_iproute2_fail(self, mock): mock.side_effect = subprocess.CalledProcessError(1, '', 'ERR') with self.assertLogs() as cm: res = SystemConfigState.query_iproute2() mock.assert_called_with(['ip', '-d', '-j', 'addr'], text=True) self.assertIsNone(res) self.assertIn('CRITICAL:root:Cannot query iproute2 interface data:', cm.output[0]) @patch('subprocess.check_output') def test_query_networkd(self, mock): mock.return_value = NETWORKD res = SystemConfigState.query_networkd() mock.assert_called_with(['networkctl', '--json=short'], text=True) self.assertEqual(len(res), 10) self.assertListEqual([itf.get('Name') for itf in res], ['lo', 'enp0s31f6', 'wlan0', 'wg0', 'wwan0', 'tun0', 'mybr0', 'mybond0', 'myvrf0', 'tun1']) @patch('subprocess.check_output') def test_query_networkd_fail(self, mock): mock.side_effect = subprocess.CalledProcessError(1, '', 'ERR') with self.assertLogs() as cm: res = SystemConfigState.query_networkd() mock.assert_called_with(['networkctl', '--json=short'], text=True) self.assertIsNone(res) self.assertIn('CRITICAL:root:Cannot query networkd interface data:', cm.output[0]) @patch('subprocess.check_output') def test_query_nm(self, mock): mock.return_value = NMCLI res = SystemConfigState.query_nm() mock.assert_called_with(['nmcli', '-t', '-f', 'DEVICE,NAME,UUID,FILENAME,TYPE,AUTOCONNECT', 'con', 'show'], text=True) self.assertEqual(len(res), 1) self.assertListEqual([itf.get('device') for itf in res], ['wlan0']) @patch('subprocess.check_output') def test_query_nm_fail(self, mock): mock.side_effect = subprocess.CalledProcessError(1, '', 'ERR') with self.assertLogs(level='DEBUG') as cm: res = SystemConfigState.query_nm() mock.assert_called_with(['nmcli', '-t', '-f', 'DEVICE,NAME,UUID,FILENAME,TYPE,AUTOCONNECT', 'con', 'show'], text=True) self.assertIsNone(res) self.assertIn('DEBUG:root:Cannot query NetworkManager interface data:', cm.output[0]) @patch('subprocess.check_output') def test_query_routes(self, mock): mock.side_effect = [ROUTE4, ROUTE6] res4, res6 = SystemConfigState.query_routes() mock.assert_has_calls([ call(['ip', '-d', '-j', '-4', 'route', 'show', 'table', 'all'], text=True), call(['ip', '-d', '-j', '-6', 'route', 'show', 'table', 'all'], text=True), ]) self.assertEqual(len(res4), 7) self.assertListEqual([route.get('dev') for route in res4], ['enp0s31f6', 'wlan0', 'wg0', 'enp0s31f6', 'wlan0', 'enp0s31f6', 'enp0s31f6']) self.assertEqual(len(res6), 10) self.assertListEqual([route.get('dev') for route in res6], ['lo', 'enp0s31f6', 'wlan0', 'enp0s31f6', 'wlan0', 'tun0', 'enp0s31f6', 'wlan0', 'enp0s31f6', 'wlan0']) @patch('subprocess.check_output') def test_query_routes_fail(self, mock): mock.side_effect = subprocess.CalledProcessError(1, '', 'ERR') with self.assertLogs(level='DEBUG') as cm: res4, res6 = SystemConfigState.query_routes() mock.assert_called_with(['ip', '-d', '-j', '-4', 'route', 'show', 'table', 'all'], text=True) self.assertIsNone(res4) self.assertIsNone(res6) self.assertIn('DEBUG:root:Cannot query iproute2 route data:', cm.output[0]) @patch('dbus.Interface') @patch('dbus.SystemBus') def test_query_resolved(self, mock_ipc, mock_iface): mock_ipc.return_value = resolve1_ipc_mock() mock_iface.return_value = resolve1_iface_mock('foo', 'bar') addresses, search = SystemConfigState.query_resolved() self.assertEqual(len(addresses), 4) self.assertListEqual([addr[0] for addr in addresses], [5, 5, 2, 2]) # interface index self.assertEqual(len(search), 2) self.assertListEqual([s[1] for s in search], ['search.domain', 'search.domain']) @patch('dbus.SystemBus') def test_query_resolved_fail(self, mock): mock.return_value = resolve1_ipc_mock() mock.side_effect = Exception(1, '', 'ERR') with self.assertLogs(level='DEBUG') as cm: addresses, search = SystemConfigState.query_resolved() self.assertIsNone(addresses) self.assertIsNone(search) self.assertIn('DEBUG:root:Cannot query resolved DNS data:', cm.output[0]) def test_query_resolvconf(self): with patch('builtins.open', mock_open(read_data='''\ nameserver 1.1.1.1 nameserver 8.8.8.8 options edns0 trust-ad search some.domain search search.domain another.one ''')): res = SystemConfigState.resolvconf_json() print(res) self.assertListEqual(res.get('addresses'), ['1.1.1.1', '8.8.8.8']) self.assertListEqual(res.get('search'), ['search.domain', 'another.one']) self.assertEqual(res.get('mode'), None) def test_query_resolvconf_stub(self): with patch('builtins.open', mock_open(read_data='\ # This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).')): res = SystemConfigState.resolvconf_json() self.assertEqual(res.get('mode'), 'stub') def test_query_resolvconf_compat(self): with patch('builtins.open', mock_open(read_data='\ # This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8).')): res = SystemConfigState.resolvconf_json() self.assertEqual(res.get('mode'), 'compat') def test_query_resolvconf_fail(self): with self.assertLogs() as cm: with patch('builtins.open', mock_open(read_data='')) as mock_file: mock_file.side_effect = Exception(1, '', 'ERR') SystemConfigState.resolvconf_json() self.assertIn('WARNING:root:Cannot parse /etc/resolv.conf:', cm.output[0]) def test_query_online_state_online(self): dev = copy.deepcopy(FAKE_DEV) dev['addr_info'] = [{ 'local': '192.168.0.100', 'prefixlen': 24, }] dev['flags'].append('UP') dev['operstate'] = 'UP' routes = [{ 'dst': 'default', 'gateway': '192.168.0.1', 'dev': dev['ifname'], }] dns = [(FAKE_DEV['ifindex'], 2, DNS_IP4)] res = SystemConfigState.query_online_state([Interface(dev, [], [], (dns, None), (routes, None))]) self.assertTrue(res) def test_query_online_state_offline(self): res = SystemConfigState.query_online_state([Interface(FAKE_DEV, [])]) self.assertFalse(res) @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_members') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_system_state_config_data_interfaces(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, members_mock, systemctl_mock): systemctl_mock.return_value = None members_mock.return_value = [] iproute2_mock.return_value = [FAKE_DEV, BRIDGE] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False networkd_mock.return_value = SystemConfigState.process_networkd(NETWORKD) state = SystemConfigState() self.assertIn('fakedev0', [iface.name for iface in state.interface_list]) @patch('subprocess.check_output') def test_query_members(self, mock): mock.return_value = '[{"ifname":"eth0"}, {"ifname":"eth1"}]' members = SystemConfigState.query_members('mybr0') mock.assert_has_calls([ call(['ip', '-d', '-j', 'link', 'show', 'master', 'mybr0'], text=True), # wokeignore:rule=master ]) self.assertListEqual(members, ['eth0', 'eth1']) @patch('subprocess.check_output') def test_query_members_fail(self, mock): mock.side_effect = subprocess.CalledProcessError(1, '', 'ERR') with self.assertLogs(level='WARNING') as cm: bridge = SystemConfigState.query_members('mybr0') mock.assert_has_calls([ call(['ip', '-d', '-j', 'link', 'show', 'master', 'mybr0'], text=True), # wokeignore:rule=master ]) self.assertListEqual(bridge, []) self.assertIn('WARNING:root:Cannot query bridge:', cm.output[0]) @patch('netplan_cli.cli.state.SystemConfigState.query_members') def test_correlate_members_and_uplink_bridge(self, mock): mock.side_effect = lambda _: ['eth0', 'eth1'] interface1 = Interface({'ifname': 'eth0'}) interface1.nd = {'Type': 'ether'} interface2 = Interface({'ifname': 'eth1'}) interface2.nd = {'Type': 'ether'} interface3 = Interface({'ifname': 'br0'}) interface3.nd = {'Type': 'bridge'} SystemConfigState.correlate_members_and_uplink([interface1, interface2, interface3]) self.assertEqual(interface1.bridge, 'br0') self.assertEqual(interface2.bridge, 'br0') self.assertListEqual(interface3.members, ['eth0', 'eth1']) @patch('netplan_cli.cli.state.SystemConfigState.query_members') def test_correlate_members_and_uplink_bond(self, mock): mock.side_effect = lambda _: ['eth2', 'eth3'] interface1 = Interface({'ifname': 'eth2'}) interface1.nd = {'Type': 'ether'} interface2 = Interface({'ifname': 'eth3'}) interface2.nd = {'Type': 'ether'} interface3 = Interface({'ifname': 'bond0'}) interface3.nd = {'Type': 'bond'} SystemConfigState.correlate_members_and_uplink([interface1, interface2, interface3]) self.assertEqual(interface1.bond, 'bond0') self.assertEqual(interface2.bond, 'bond0') self.assertListEqual(interface3.members, ['eth2', 'eth3']) @patch('netplan_cli.cli.state.SystemConfigState.query_members') def test_correlate_members_and_uplink_vrf(self, mock): mock.side_effect = lambda _: ['eth2', 'eth3'] interface1 = Interface({'ifname': 'eth2'}) interface1.nd = {'Type': 'ether'} interface2 = Interface({'ifname': 'eth3'}) interface2.nd = {'Type': 'ether'} interface3 = Interface({'ifname': 'vrf0'}) interface3.nd = {'Type': 'ether', 'Kind': 'vrf'} SystemConfigState.correlate_members_and_uplink([interface1, interface2, interface3]) self.assertEqual(interface1.vrf, 'vrf0') self.assertEqual(interface2.vrf, 'vrf0') self.assertListEqual(interface3.members, ['eth2', 'eth3']) class TestNetplanState(unittest.TestCase): '''Test netplan state NetplanConfigState class''' def setUp(self): self.workdir = tempfile.TemporaryDirectory(prefix='netplan_') self.file = '70-netplan-set.yaml' self.path = os.path.join(self.workdir.name, 'etc', 'netplan', self.file) os.makedirs(os.path.join(self.workdir.name, 'etc', 'netplan')) with open(self.path, "w") as f: f.write('''network: ethernets: eth0: dhcp4: true bridges: br0: dhcp4: true''') def tearDown(self): shutil.rmtree(self.workdir.name) def test_get_data(self): state = NetplanConfigState(rootdir=self.workdir.name) state_data = state.get_data() self.assertIn('eth0', state_data.get('network').get('ethernets')) self.assertIn('br0', state_data.get('network').get('bridges')) def test_get_data_subtree(self): state = NetplanConfigState(subtree='ethernets', rootdir=self.workdir.name) state_data = state.get_data() self.assertIn('eth0', state_data) self.assertNotIn('br0', state_data) class TestInterface(unittest.TestCase): '''Test netplan state Interface class''' @patch('subprocess.check_output') def test_query_nm_ssid(self, mock): mock.return_value = ' MYSSID ' # added some whitespace to strip() con = 'SOME_CONNECTION_ID' itf = Interface(FAKE_DEV, []) res = itf.query_nm_ssid(con) mock.assert_called_with(['nmcli', '--get-values', '802-11-wireless.ssid', 'con', 'show', 'id', con], text=True) self.assertEqual(res, 'MYSSID') @patch('subprocess.check_output') def test_query_nm_ssid_fail(self, mock): mock.side_effect = subprocess.CalledProcessError(1, '', 'ERR') con = 'SOME_CONNECTION_ID' itf = Interface(FAKE_DEV, []) with self.assertLogs() as cm: res = itf.query_nm_ssid(con) mock.assert_called_with(['nmcli', '--get-values', '802-11-wireless.ssid', 'con', 'show', 'id', con], text=True) self.assertIsNone(res) self.assertIn('WARNING:root:Cannot query NetworkManager SSID for {}:'.format(con), cm.output[0]) @patch('subprocess.check_output') def test_query_networkctl(self, mock): mock.return_value = 'DOES NOT MATTER' dev = 'fakedev0' itf = Interface(FAKE_DEV, []) res = itf.query_networkctl(dev) mock.assert_called_with(['networkctl', 'status', '--', dev], text=True) self.assertEqual(res, mock.return_value) @patch('subprocess.check_output') def test_query_networkctl_fail(self, mock): mock.side_effect = subprocess.CalledProcessError(1, '', 'ERR') dev = 'fakedev0' itf = Interface(FAKE_DEV, []) with self.assertLogs() as cm: res = itf.query_networkctl(dev) mock.assert_called_with(['networkctl', 'status', '--', dev], text=True) self.assertIsNone(res) self.assertIn('WARNING:root:Cannot query networkctl for {}:'.format(dev), cm.output[0]) @patch('netplan_cli.cli.state.Interface.query_nm_ssid') @patch('netplan_cli.cli.state.Interface.query_networkctl') def test_json_nm_wlan0(self, networkctl_mock, nm_ssid_mock): SSID = 'MYCON' nm_ssid_mock.return_value = SSID # networkctl mock output reduced to relevant lines networkctl_mock.return_value = \ 'WiFi access point: {} (b4:fb:e4:75:c6:21)'.format(SSID) data = next((itf for itf in yaml.safe_load(IPROUTE2) if itf['ifindex'] == 5), {}) nd = SystemConfigState.process_networkd(NETWORKD) nm = SystemConfigState.process_nm(NMCLI) dns = (DNS_ADDRESSES, DNS_SEARCH) routes = (SystemConfigState.process_generic(ROUTE4), SystemConfigState.process_generic(ROUTE6)) itf = Interface(data, nd, nm, dns, routes) self.assertTrue(itf.up) self.assertFalse(itf.down) ifname, json = itf.json() self.assertEqual(ifname, 'wlan0') self.assertEqual(json.get('index'), 5) self.assertEqual(json.get('macaddress'), '1c:4d:70:e4:e4:0e') self.assertEqual(json.get('type'), 'wifi') self.assertEqual(json.get('ssid'), 'MYCON') self.assertEqual(json.get('backend'), 'NetworkManager') self.assertEqual(json.get('id'), 'NM-b6b7a21d-186e-45e1-b3a6-636da1735563') self.assertEqual(json.get('vendor'), 'Intel Corporation') self.assertEqual(json.get('adminstate'), 'UP') self.assertEqual(json.get('operstate'), 'UP') self.assertEqual(len(json.get('addresses')), 4) self.assertEqual(len(json.get('dns_addresses')), 2) self.assertEqual(len(json.get('dns_search')), 1) self.assertEqual(len(json.get('routes')), 6) @patch('netplan_cli.cli.state.Interface.query_networkctl') def test_json_nd_enp0s31f6(self, networkctl_mock): # networkctl mock output reduced to relevant lines networkctl_mock.return_value = 'Activation Policy: manual' data = next((itf for itf in yaml.safe_load(IPROUTE2) if itf['ifindex'] == 2), {}) nd = SystemConfigState.process_networkd(NETWORKD) nm = SystemConfigState.process_nm(NMCLI) dns = (DNS_ADDRESSES, DNS_SEARCH) routes = (SystemConfigState.process_generic(ROUTE4), SystemConfigState.process_generic(ROUTE6)) itf = Interface(data, nd, nm, dns, routes) self.assertTrue(itf.up) self.assertFalse(itf.down) ifname, json = itf.json() self.assertEqual(ifname, 'enp0s31f6') self.assertEqual(json.get('index'), 2) self.assertEqual(json.get('macaddress'), '54:e1:ad:5f:24:b4') self.assertEqual(json.get('type'), 'ethernet') self.assertEqual(json.get('backend'), 'networkd') self.assertEqual(json.get('id'), 'enp0s31f6') self.assertEqual(json.get('vendor'), 'Intel Corporation') self.assertEqual(json.get('adminstate'), 'UP') self.assertEqual(json.get('operstate'), 'UP') self.assertEqual(json.get('activation_mode'), 'manual') self.assertEqual(len(json.get('addresses')), 3) _, meta = list(json.get('addresses')[0].items())[0] # get first (any only) address self.assertIn('dhcp', meta.get('flags')) self.assertEqual(len(json.get('dns_addresses')), 2) self.assertEqual(len(json.get('dns_search')), 1) self.assertEqual(len(json.get('routes')), 8) def test_json_nd_tunnel(self): data = next((itf for itf in yaml.safe_load(IPROUTE2) if itf['ifindex'] == 41), {}) nd = SystemConfigState.process_networkd(NETWORKD) itf = Interface(data, nd, [], (None, None), (None, None)) ifname, json = itf.json() self.assertEqual(ifname, 'wg0') self.assertEqual(json.get('index'), 41) self.assertEqual(json.get('type'), 'tunnel') self.assertEqual(json.get('backend'), 'networkd') self.assertEqual(json.get('tunnel_mode'), 'wireguard') def test_json_no_type_id_backend(self): itf = Interface(FAKE_DEV, [], [], (None, None), (None, None)) ifname, json = itf.json() self.assertEqual(ifname, 'fakedev0') self.assertEqual(json.get('index'), 42) self.assertNotIn('type', json) self.assertNotIn('id', json) self.assertNotIn('backend', json) netplan-1.0/tests/cli/test_state_diff.py000066400000000000000000002127671457004145200204660ustar00rootroot00000000000000#!/usr/bin/python3 # Closed-box tests of netplan CLI. These are run during "make check" and don't # touch the system configuration at all. # # Copyright (C) 2023 Canonical, Ltd. # Authors: Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import json import os import tempfile import unittest from unittest.mock import Mock from netplan.netdef import NetplanRoute from netplan_cli.cli.state import Interface, NetplanConfigState, SystemConfigState from netplan_cli.cli.state_diff import DiffJSONEncoder, NetplanDiffState class TestNetplanDiff(unittest.TestCase): '''Test netplan state NetplanDiffState class''' def setUp(self): self.workdir = tempfile.TemporaryDirectory(prefix='netplan_') self.file = '90-netplan.yaml' self.path = os.path.join(self.workdir.name, 'etc', 'netplan', self.file) os.makedirs(os.path.join(self.workdir.name, 'etc', 'netplan')) self.diff_state = NetplanDiffState(Mock(spec=SystemConfigState), Mock(spec=NetplanConfigState)) self.diff_state.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} def test_get_full_state(self): with open(self.path, "w") as f: f.write('''network: ethernets: mynic: match: name: eth0 dhcp4: true dhcp6: false''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'mynic', 'index': 2, 'type': 'ethernet', 'addresses': [ { '1.2.3.4': { 'prefix': 24, 'flags': ['dhcp'], } }, ], } } system_state.interface_list = [] diff_state = NetplanDiffState(system_state, netplan_state) full_state = diff_state.get_full_state() expected = { 'interfaces': { 'eth0': { 'system_state': { 'type': 'ethernet', 'addresses': { '1.2.3.4/24': { 'flags': ['dhcp'] } }, 'id': 'mynic', 'index': 2, }, 'netplan_state': { 'id': 'mynic', 'type': 'ethernet', 'dhcp4': True, 'dhcp6': False, 'link_local': ['ipv6'], } } } } self.assertDictEqual(full_state, expected) def test_get_netplan_interfaces(self): with open(self.path, "w") as f: f.write('''network: ethernets: mynic: match: name: eth0 dhcp4: false dhcp6: false macaddress: aa:bb:cc:dd:ee:ff routes: - to: default via: 1.2.3.4 nameservers: addresses: - 1.1.1.1 - 2.2.2.2 search: - mydomain.local addresses: - 192.168.0.2/24: label: myip lifetime: forever - 192.168.0.1/24''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'mynic', 'index': 2, } } system_state.interface_list = [] diff_state = NetplanDiffState(system_state, netplan_state) interfaces = diff_state._get_netplan_interfaces() expected = { 'eth0': { 'netplan_state': { 'id': 'mynic', 'addresses': { '192.168.0.1/24': { 'flags': {} }, '192.168.0.2/24': { 'flags': {'label': 'myip', 'lifetime': 'forever'}, } }, 'dhcp4': False, 'dhcp6': False, 'link_local': ['ipv6'], 'nameservers_addresses': ['1.1.1.1', '2.2.2.2'], 'nameservers_search': ['mydomain.local'], 'macaddress': 'aa:bb:cc:dd:ee:ff', 'type': 'ethernet', 'routes': [NetplanRoute(to='default', via='1.2.3.4', family=2)], } } } self.assertDictEqual(interfaces, expected) def test_get_netplan_interfaces_with_match(self): with open(self.path, "w") as f: f.write('''network: ethernets: # not matching any physical device myeths: match: name: eth* mynics: dhcp4: false dhcp6: false match: name: enp0*''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'enp0s3': { 'name': 'enp0s3', 'id': 'mynics', 'index': 2, }, 'enp0s4': { 'name': 'enp0s4', 'id': 'mynics', 'index': 3, }, 'enp0s5': { 'name': 'enp0s5', 'id': 'mynics', 'index': 4, } } system_state.interface_list = [] diff_state = NetplanDiffState(system_state, netplan_state) interfaces = diff_state._get_netplan_interfaces() expected = { 'enp0s3': { 'netplan_state': { 'id': 'mynics', 'dhcp4': False, 'dhcp6': False, 'type': 'ethernet', 'link_local': ['ipv6'], } }, 'enp0s4': { 'netplan_state': { 'id': 'mynics', 'dhcp4': False, 'dhcp6': False, 'type': 'ethernet', 'link_local': ['ipv6'], } }, 'enp0s5': { 'netplan_state': { 'id': 'mynics', 'dhcp4': False, 'dhcp6': False, 'type': 'ethernet', 'link_local': ['ipv6'], }, }, 'myeths': { 'netplan_state': { 'id': 'myeths', 'dhcp4': False, 'dhcp6': False, 'type': 'ethernet', 'link_local': ['ipv6'], } } } self.assertDictEqual(interfaces, expected) def test_get_system_interfaces(self): system_state = Mock(spec=SystemConfigState) netplan_state = Mock(spec=NetplanConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'mynic', 'type': 'ethernet', 'index': 2, 'addresses': [ { '1.2.3.4': { 'prefix': 24, 'flags': [], } }, ], 'dns_addresses': ['1.1.1.1', '2.2.2.2'], 'dns_search': ['mydomain.local'], 'routes': [ { 'to': 'default', 'via': '192.168.5.1', 'from': '192.168.5.122', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'family': 2, 'table': 'main' } ], 'macaddress': 'aa:bb:cc:dd:ee:ff', } } system_state.interface_list = [] diff_state = NetplanDiffState(system_state, netplan_state) diff_state.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} interfaces = diff_state._get_system_interfaces() expected = { 'eth0': { 'system_state': { 'type': 'ethernet', 'id': 'mynic', 'index': 2, 'addresses': { '1.2.3.4/24': { 'flags': [] } }, 'nameservers_addresses': ['1.1.1.1', '2.2.2.2'], 'nameservers_search': ['mydomain.local'], 'routes': [ NetplanRoute(to='default', via='192.168.5.1', from_addr='192.168.5.122', type='unicast', scope='global', protocol='kernel', table=254, family=2, metric=100) ], 'macaddress': 'aa:bb:cc:dd:ee:ff' }, } } self.assertDictEqual(interfaces, expected) def test_diff_default_table_names_to_number(self): self.assertEqual(self.diff_state._default_route_tables_name_to_number('main'), 254) self.assertEqual(self.diff_state._default_route_tables_name_to_number('default'), 253) self.assertEqual(self.diff_state._default_route_tables_name_to_number('local'), 255) self.assertEqual(self.diff_state._default_route_tables_name_to_number('1000'), 1000) self.assertEqual(self.diff_state._default_route_tables_name_to_number('blah'), 0) def test__system_route_to_netplan_empty_input(self): route = self.diff_state._system_route_to_netplan({}) expected = NetplanRoute() self.assertEqual(route, expected) def test__system_route_to_netplan(self): route = { 'to': 'default', 'via': '192.168.5.1', 'from': '192.168.5.122', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'family': 2, 'table': 'main' } netplan_route = self.diff_state._system_route_to_netplan(route) expected = NetplanRoute(to='default', via='192.168.5.1', from_addr='192.168.5.122', metric=100, type='unicast', scope='global', protocol='kernel', family=2, table=254) self.assertEqual(netplan_route, expected) def test_diff_missing_netplan_interface(self): with open(self.path, "w") as f: f.write('''network: ethernets: {}''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'index': 2, }, 'lo': { 'name': 'lo', 'index': 1, } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'lo' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('missing_interfaces_netplan', []) self.assertIn('eth0', missing) # lo is included self.assertIn('lo', missing) def test_diff_missing_system_interface(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} eth1: {}''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, } } interface = Mock(spec=Interface) interface.name = 'eth0' interface.netdef_id = 'eth0' system_state.interface_list = [interface] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('missing_interfaces_system', []) self.assertIn('eth1', missing) def test_diff_missing_system_interface_with_match(self): with open(self.path, "w") as f: f.write('''network: ethernets: mynics: dhcp4: false match: name: eth*''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'enp0s1': { 'name': 'enp0s1', 'id': 'enp0s1', 'index': 2, } } interface1 = Mock(spec=Interface) interface1.name = 'enp0s1' interface1.netdef_id = 'enp0s1' system_state.interface_list = [interface1] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('missing_interfaces_system', []) self.assertIn('mynics', missing) def test_diff_not_missing_system_interface_with_match(self): with open(self.path, "w") as f: f.write('''network: ethernets: mynics: dhcp4: false match: name: eth*''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'mynics', 'index': 2, }, 'enp0s1': { 'name': 'enp0s1', 'id': 'enp0s1', 'index': 3, } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'mynics' interface2 = Mock(spec=Interface) interface2.name = 'enp0s1' interface2.netdef_id = 'enp0s1' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('missing_interfaces_system', {}) self.assertDictEqual(missing, {}) def test__get_comparable_interfaces_empty(self): res = self.diff_state._get_comparable_interfaces({}) self.assertDictEqual(res, {}) def test__get_comparable_interfaces(self): input = { 'eth0': { 'system_state': { 'id': 'eth0' }, 'netplan_state': {} }, 'eth1': { 'netplan_state': {} }, 'eth2': { 'system_state': {} }, 'eth3': { 'system_state': {}, 'netplan_state': {} }, } res = self.diff_state._get_comparable_interfaces(input) self.assertDictEqual(res, {'eth0': {'netplan_state': {}, 'system_state': {'id': 'eth0'}}}) def test__compress_ipv6_address_with_prefix(self): self.assertEqual(self.diff_state._compress_ipv6_address('a:b:c:0:0:0::d/64'), 'a:b:c::d/64') def test__compress_ipv6_address_without_prefix(self): self.assertEqual(self.diff_state._compress_ipv6_address('a:b:c:0:0:0::d'), 'a:b:c::d') def test__compress_ipv6_address_ipv4_with_prefix(self): self.assertEqual(self.diff_state._compress_ipv6_address('192.168.0.1/24'), '192.168.0.1/24') def test__compress_ipv6_address_ipv4_without_prefix(self): self.assertEqual(self.diff_state._compress_ipv6_address('192.168.0.1'), '192.168.0.1') def test__compress_ipv6_address_not_an_ip(self): self.assertEqual(self.diff_state._compress_ipv6_address('default'), 'default') def test__normalize_ip_addresses(self): ips = {'abcd:0:0:0::1/64', '1:2:0:0::123', '1.2.3.4/24', '1.2.3.4'} expected = {'abcd::1/64', '1:2::123', '1.2.3.4/24', '1.2.3.4'} result = self.diff_state._normalize_ip_addresses(ips) self.assertSetEqual(expected, result) def test_diff_missing_system_address(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: dhcp4: false dhcp6: false addresses: - 192.168.0.2/24: label: myip lifetime: forever - 192.168.0.1/24''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_addresses', []) self.assertIn('192.168.0.1/24', missing) self.assertIn('192.168.0.2/24', missing) def test_diff_missing_system_address_with_match(self): with open(self.path, "w") as f: f.write('''network: ethernets: mynic: match: name: "eth*" dhcp4: false dhcp6: false addresses: - 192.168.0.2/24: label: myip lifetime: forever - 192.168.0.1/24''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'mynic', 'index': 2, } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'mynic' system_state.interface_list = [interface1] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_addresses', []) self.assertIn('192.168.0.1/24', missing) self.assertIn('192.168.0.2/24', missing) def test_diff_dhcp_addresses_are_filtered_out(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: dhcp4: true dhcp6: true''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'addresses': [ {'192.168.0.1': {'prefix': 24, 'flags': ['dhcp']}}, {'192.168.254.1': {'prefix': 24, 'flags': ['dhcp']}}, {'abcd:1234::1': {'prefix': 64, 'flags': ['dhcp']}} ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_addresses', []) self.assertEqual(missing, []) def test_diff_missing_netplan_address(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: dhcp4: false dhcp6: false addresses: - 192.168.0.1/24''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'addresses': [ {'192.168.0.1': {'prefix': 24}}, {'192.168.254.1': {'prefix': 24}} ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_addresses', []) self.assertIn('192.168.254.1/24', missing) diff_data = diff.get_diff('eth0') missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_addresses', []) self.assertIn('192.168.254.1/24', missing) # eth1 does not exist diff_data = diff.get_diff('eth1') self.assertDictEqual(diff_data['interfaces'], {}) def test_diff_addresses_compressed_ipv6(self): ''' Check if IPv6 address will not mismatch due to their representation''' with open(self.path, "w") as f: f.write('''network: ethernets: eth0: dhcp4: false dhcp6: false addresses: - 1:2:3:0:0:0::123/64''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'addresses': [ {'1:2:3::123': {'prefix': 64}}, ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing_netplan = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_addresses', []) missing_system = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_addresses', []) self.assertListEqual([], missing_netplan) self.assertListEqual([], missing_system) def test_diff_missing_system_dhcp_addresses(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: dhcp4: true dhcp6: true''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() dhcp4 = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_dhcp4_address') dhcp6 = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_dhcp6_address') self.assertTrue(dhcp4) self.assertTrue(dhcp6) def test_diff_link_local_addresses(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: dhcp4: false dhcp6: false link-local: []''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'addresses': [ {'169.254.65.85': {'prefix': 16, 'flags': ['link']}}, {'fe80::4e7:f4ff:fe6e:c917': {'prefix': 64, 'flags': ['link']}}, ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_addresses', []) self.assertIn('169.254.65.85/16', missing) self.assertIn('fe80::4e7:f4ff:fe6e:c917/64', missing) def test_diff_missing_system_nameservers(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: nameservers: addresses: - 1.2.3.4 - 4.3.2.1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_nameservers_addresses', []) self.assertIn('1.2.3.4', missing) self.assertIn('4.3.2.1', missing) def test_diff_missing_netplan_nameservers(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {}''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'dns_addresses': ['1.2.3.4', '4.3.2.1'], } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() state = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}) missing = state.get('missing_nameservers_addresses', []) self.assertIn('1.2.3.4', missing) self.assertIn('4.3.2.1', missing) def test_diff_missing_system_nameservers_with_match(self): with open(self.path, "w") as f: f.write('''network: ethernets: mynic: match: name: eth0 nameservers: addresses: - 1.2.3.4 - 4.3.2.1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'mynic', 'index': 2, } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'mynic' system_state.interface_list = [interface1] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_nameservers_addresses', []) self.assertIn('1.2.3.4', missing) self.assertIn('4.3.2.1', missing) def test_diff_missing_system_nameservers_search(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: nameservers: search: - mydomain.local - anotherdomain.local''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_nameservers_search', []) self.assertIn('mydomain.local', missing) self.assertIn('anotherdomain.local', missing) def test_diff_missing_netplan_nameservers_search(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: dhcp4: false dhcp6: false''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'dns_search': ['mydomain.local', 'anotherdomain.local'], } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_nameservers_search', []) self.assertIn('mydomain.local', missing) self.assertIn('anotherdomain.local', missing) def test_diff_missing_system_nameservers_search_with_match(self): with open(self.path, "w") as f: f.write('''network: ethernets: mynic: match: name: eth0 nameservers: search: - mydomain.local - anotherdomain.local''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'mynic', 'index': 2, } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'mynic' system_state.interface_list = [interface1] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_nameservers_search', []) self.assertIn('mydomain.local', missing) self.assertIn('anotherdomain.local', missing) def test_diff_macaddress(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: macaddress: aa:bb:cc:dd:ee:ff''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'macaddress': '11:22:33:44:55:66' } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'mynic' system_state.interface_list = [interface1] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing_system = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_macaddress') missing_netplan = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_macaddress') self.assertEqual(missing_system, 'aa:bb:cc:dd:ee:ff') self.assertEqual(missing_netplan, '11:22:33:44:55:66') def test_diff_macaddress_option(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: macaddress: random''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'macaddress': '11:22:33:44:55:66' } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'mynic' system_state.interface_list = [interface1] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() # if the macaddress value in Netplan is one of the supported options, such as 'random', we don't calculated a diff missing_system = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_macaddress') missing_netplan = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_macaddress') self.assertEqual(missing_system, None) self.assertEqual(missing_netplan, None) def test_diff_macaddress_with_match(self): with open(self.path, "w") as f: f.write('''network: ethernets: mynic: match: name: eth0 macaddress: aa:bb:cc:dd:ee:ff''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'mynic', 'index': 2, 'macaddress': '11:22:33:44:55:66' } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'mynic' system_state.interface_list = [interface1] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing_system = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_macaddress') missing_netplan = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_macaddress') self.assertEqual(missing_system, 'aa:bb:cc:dd:ee:ff') self.assertEqual(missing_netplan, '11:22:33:44:55:66') def test__filter_system_routes_empty_inputs(self): filtered = self.diff_state._filter_system_routes(set(), [], {}) self.assertSetEqual(filtered, set()) def test__filter_system_routes_link_scope_routes(self): route = NetplanRoute(to='1.2.3.0/24', scope='link') filtered = self.diff_state._filter_system_routes({route}, [], {}) self.assertSetEqual(filtered, set()) def test__filter_system_routes_dhcp_ra_routes(self): route1 = NetplanRoute(protocol='dhcp') route2 = NetplanRoute(protocol='ra') filtered = self.diff_state._filter_system_routes({route1, route2}, [], {}) self.assertSetEqual(filtered, set()) def test__filter_system_routes_link_local_routes(self): route1 = NetplanRoute(scope='host', type='local', to='1.2.3.4', from_addr='1.2.3.4') # local 127.0.0.0/8 dev lo table local proto kernel scope host src 127.0.0.1 route2 = NetplanRoute(scope='host', type='local', to='127.0.0.0/8', from_addr='127.0.0.1') system_addresses = ['1.2.3.4/24', '127.0.0.1/8'] filtered = self.diff_state._filter_system_routes({route1, route2}, system_addresses, {}) self.assertSetEqual(filtered, set()) def test__filter_system_routes_keep_link_local_routes(self): route1 = NetplanRoute(scope='link', type='unicast', to='169.254.0.0/16', family=2, from_addr='169.254.65.85') route2 = NetplanRoute(scope='global', type='unicast', to='fe80::/64', family=10) system_addresses = [] config = {'netplan_state': {'link_local': []}} filtered = self.diff_state._filter_system_routes({route1, route2}, system_addresses, config) # link local routes are present but they are disabled in the netdef self.assertSetEqual(filtered, {route1, route2}) def test__filter_system_routes_remove_link_local_routes(self): route1 = NetplanRoute(scope='link', type='unicast', to='169.254.0.0/16', family=2, from_addr='169.254.65.85') route2 = NetplanRoute(scope='global', type='unicast', to='fe80::/64', family=10) system_addresses = [] config = {'netplan_state': {'link_local': ['ipv4', 'ipv6']}} filtered = self.diff_state._filter_system_routes({route1, route2}, system_addresses, config) # link local routes are present and link local is enabled for both ipv4 and ipv6 in the netdef self.assertSetEqual(filtered, set()) def test__filter_system_routes_link_local_routes_with_multiple_ips_same_subnet(self): # When an interface has multiple IPs in the same subnet the routing table will # have routes using one of the IPs as source. Example: # local 192.168.0.123 dev eth0 table local proto kernel scope host src 192.168.0.123 # local 192.168.0.124 dev eth0 table local proto kernel scope host src 192.168.0.123 route1 = NetplanRoute(scope='host', type='local', to='1.2.3.4', from_addr='1.2.3.4') route2 = NetplanRoute(scope='host', type='local', to='1.2.3.5', from_addr='1.2.3.4') system_addresses = ['1.2.3.4/24', '1.2.3.5/24'] filtered = self.diff_state._filter_system_routes({route1, route2}, system_addresses, {}) self.assertSetEqual(filtered, set()) def test__filter_system_routes_ipv6_multicast_routes(self): route = NetplanRoute(type='multicast', to='ff00::/8', family=10) filtered = self.diff_state._filter_system_routes({route}, [], {}) self.assertSetEqual(filtered, set()) def test__filter_system_routes_ipv6_host_local_routes(self): route1 = NetplanRoute(family=10, to='fd42:bc43:e20e:8cf7:216:3eff:feaf:4121') route2 = NetplanRoute(family=10, to='fd42:bc43:e20e:8cf7::/64') addresses = ['fd42:bc43:e20e:8cf7:216:3eff:feaf:4121/64'] filtered = self.diff_state._filter_system_routes({route1, route2}, addresses, {}) self.assertSetEqual(filtered, set()) def test__filter_system_routes_should_not_be_filtered(self): route1 = NetplanRoute(to='default', via='1.2.3.4') route2 = NetplanRoute(to='1.2.3.0/24', via='4.3.2.1') route3 = NetplanRoute(to='1:2:3::/64', via='1:2:3::1234') filtered = self.diff_state._filter_system_routes({route1, route2, route3}, [], {}) self.assertSetEqual(filtered, {route1, route2, route3}) def test_diff_missing_netplan_routes(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {}''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'addresses': [{'fd42:bc43:e20e:8cf7:216:3eff:feaf:4121': {'prefix': 64}}], 'routes': [ { 'to': 'default', 'via': '192.168.5.1', 'from': '192.168.5.122', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'family': 2, 'table': 'main' }, { 'to': '192.168.5.0', 'via': '192.168.5.1', 'from': '192.168.5.122', 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'family': 2, 'table': 'main' }, { 'to': '1.2.3.0/24', 'via': '192.168.5.1', 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'family': 2, 'table': 'main' }, { 'to': 'abcd::/64', 'via': 'abcd::1', 'type': 'unicast', 'scope': 'global', 'protocol': 'ra', 'family': 10, 'table': 'main' }, { 'to': 'fe80::/64', 'protocol': 'kernel', 'family': 10, 'table': 'main' }, { 'type': 'multicast', 'to': 'ff00::/8', 'table': 'local', 'protocol': 'kernel', 'family': 10 }, { 'type': 'local', 'to': '10.86.126.148', 'table': 'local', 'protocol': 'kernel', 'scope': 'host', 'from': '10.86.126.148', 'family': 2 }, { 'type': 'local', 'to': 'fd42:bc43:e20e:8cf7:216:3eff:feaf:4121', 'table': 'local', 'protocol': 'kernel', 'scope': 'global', 'family': 10 } ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} diff_data = diff.get_diff() expected = {} expected['to'] = 'default' expected['via'] = '192.168.5.1' expected['from_addr'] = '192.168.5.122' expected['metric'] = 100 expected['protocol'] = 'kernel' expected['family'] = 2 expected['table'] = 254 expected_route = NetplanRoute(**expected) missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_routes', []) self.assertIn(expected_route, missing) def test_diff_missing_system_routes(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: routes: - to: 1.2.3.0/24 via: 192.168.0.1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'routes': [ { 'to': 'default', 'via': '192.168.5.1', 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'family': 2, 'table': 'main' } ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} diff_data = diff.get_diff() expected = {} expected['to'] = '1.2.3.0/24' expected['via'] = '192.168.0.1' expected['family'] = 2 expected['table'] = 254 expected_route = NetplanRoute(**expected) missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_routes', []) self.assertEqual(expected_route, missing[0]) def test_diff_missing_system_routes_with_match(self): with open(self.path, "w") as f: f.write('''network: ethernets: mynic: match: name: eth0 routes: - to: 1.2.3.0/24 via: 192.168.0.1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'mynic', 'index': 2, 'routes': [ { 'to': 'default', 'via': '192.168.5.1', 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'family': 2, 'table': 'main' } ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} diff_data = diff.get_diff() expected = {} expected['to'] = '1.2.3.0/24' expected['via'] = '192.168.0.1' expected['family'] = 2 expected['table'] = 254 expected_route = NetplanRoute(**expected) missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_routes', []) self.assertEqual(expected_route, missing[0]) def test_diff_slash_32_and_128_routes(self): ''' /32 and /128 route entries from "ip route show" will not have the prefix 1.2.3.4 via 10.3.0.1 dev mainif proto static 1:2:3::4 via 10:3::1 dev mainif proto static metric 1024 pref medium ''' with open(self.path, "w") as f: f.write('''network: ethernets: eth0: routes: - to: 1:2:3::4/128 via: 1:2:3::1 - to: 1.2.3.4/32 via: 192.168.0.1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'routes': [ { 'to': '1.2.3.4', 'via': '192.168.0.1', 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'family': 2, 'table': 'main' }, { 'to': '1:2:3::4', 'via': '1:2:3::1', 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'family': 10, 'table': 'main' } ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing_system = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_routes', []) missing_netplan = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_routes', []) self.assertListEqual([], missing_system) self.assertListEqual([], missing_netplan) def test_diff_compressed_ipv6_routes(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: routes: - to: 1:2:3:0:0:0::4/64 from: 1:2:3:0:0:0::123 via: 1:2:3:0:0::1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'routes': [ { 'to': '1:2:3::4/64', 'via': '1:2:3::1', 'from': '1:2:3::123', 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'family': 10, 'table': 'main' } ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing_system = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_routes', []) missing_netplan = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_routes', []) self.assertListEqual([], missing_system) self.assertListEqual([], missing_netplan) def test_diff_json_encoder(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: routes: - to: 1.2.3.0/24 via: 192.168.0.1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'routes': [ { 'to': 'default', 'via': '192.168.5.1', 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'family': 2, 'table': 'main' } ] } } system_state.interface_list = [] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() diff_data_str = json.dumps(diff_data, cls=DiffJSONEncoder) diff_data_dict = json.loads(diff_data_str) self.assertTrue(len(diff_data_dict['interfaces']['eth0']['system_state']['missing_routes']) > 0) self.assertTrue(len(diff_data_dict['interfaces']['eth0']['netplan_state']['missing_routes']) > 0) def test_diff_present_system_bond_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} bonds: bond0: interfaces: - eth0''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'bond': 'bond0' }, 'bond0': { 'name': 'bond0', 'id': 'bond0', 'index': 3, 'interfaces': ['eth0'], } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'bond0' interface2.netdef_id = 'bond0' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_bond_link') self.assertIsNone(missing) missing = diff_data.get('interfaces', {}).get('bond0', {}).get('system_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, []) def test_diff_missing_system_bond_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} bonds: bond0: interfaces: - eth0''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, }, 'bond0': { 'name': 'bond0', 'id': 'bond0', 'index': 3, } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'bond0' interface2.netdef_id = 'bond0' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_bond_link') self.assertEqual(missing, 'bond0') missing = diff_data.get('interfaces', {}).get('bond0', {}).get('system_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, ['eth0']) def test_diff_present_system_bridge_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} bridges: br0: interfaces: - eth0''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'bridge': 'br0' }, 'br0': { 'name': 'br0', 'id': 'br0', 'index': 3, 'interfaces': ['eth0'], } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'br0' interface2.netdef_id = 'br0' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_bridge_link') self.assertIsNone(missing) missing = diff_data.get('interfaces', {}).get('br0', {}).get('system_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, []) def test_diff_missing_system_bridge_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} bridges: br0: interfaces: - eth0''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, }, 'br0': { 'name': 'br0', 'id': 'br0', 'index': 3, } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'br0' interface2.netdef_id = 'br0' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_bridge_link') self.assertEqual(missing, 'br0') missing = diff_data.get('interfaces', {}).get('br0', {}).get('system_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, ['eth0']) def test_diff_present_system_vrf_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} vrfs: vrf0: table: 1000 interfaces: - eth0''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'vrf': 'vrf0' }, 'vrf0': { 'name': 'vrf0', 'id': 'vrf0', 'index': 3, 'interfaces': ['eth0'], } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'vrf0' interface2.netdef_id = 'vrf0' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_vrf_link') self.assertIsNone(missing) missing = diff_data.get('interfaces', {}).get('vrf0', {}).get('system_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, []) def test_diff_missing_system_vrf_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} vrfs: vrf0: table: 1000 interfaces: - eth0''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, }, 'vrf0': { 'name': 'vrf0', 'id': 'vrf0', 'index': 3, } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'vrf0' interface2.netdef_id = 'vrf0' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('system_state', {}).get('missing_vrf_link') self.assertEqual(missing, 'vrf0') missing = diff_data.get('interfaces', {}).get('vrf0', {}).get('system_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, ['eth0']) def test_diff_present_netplan_bond_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} bonds: bond0: interfaces: - eth0''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'bond': 'bond0' }, 'bond0': { 'name': 'bond0', 'id': 'bond0', 'index': 3, 'interfaces': ['eth0'], } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'bond0' interface2.netdef_id = 'bond0' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_bond_link') self.assertIsNone(missing) missing = diff_data.get('interfaces', {}).get('bond0', {}).get('netplan_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, []) def test_diff_missing_netplan_bond_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} eth1: {} bonds: bond0: interfaces: - eth1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'bond': 'bond0', }, 'eth1': { 'name': 'eth0', 'id': 'eth0', 'index': 3, 'bond': 'bond0', }, 'bond0': { 'name': 'bond0', 'id': 'bond0', 'index': 4, 'interfaces': ['eth0', 'eth1'] } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'eth1' interface2.netdef_id = 'eth1' interface3 = Mock(spec=Interface) interface3.name = 'bond0' interface3.netdef_id = 'bond0' system_state.interface_list = [interface1, interface2, interface3] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_bond_link') self.assertEqual(missing, 'bond0') missing = diff_data.get('interfaces', {}).get('bond0', {}).get('netplan_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, ['eth0']) def test_diff_present_netplan_bridge_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} bridges: br0: interfaces: - eth0''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'bridge': 'br0' }, 'br0': { 'name': 'br0', 'id': 'br0', 'index': 3, 'interfaces': ['eth0'], } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'br0' interface2.netdef_id = 'br0' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_bridge_link') self.assertIsNone(missing) missing = diff_data.get('interfaces', {}).get('br0', {}).get('netplan_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, []) def test_diff_missing_netplan_bridge_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} eth1: {} bridges: br0: interfaces: - eth1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'bridge': 'br0', }, 'eth1': { 'name': 'eth1', 'id': 'eth1', 'index': 3, 'bridge': 'br0', }, 'br0': { 'name': 'br0', 'id': 'br0', 'index': 4, 'interfaces': ['eth0', 'eth1'] } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'eth1' interface2.netdef_id = 'eth1' interface3 = Mock(spec=Interface) interface3.name = 'br0' interface3.netdef_id = 'br0' system_state.interface_list = [interface1, interface2, interface3] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_bridge_link') self.assertEqual(missing, 'br0') missing = diff_data.get('interfaces', {}).get('br0', {}).get('netplan_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, ['eth0']) def test_diff_present_netplan_vrf_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} vrfs: vrf0: table: 1000 interfaces: - eth0''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'vrf': 'vrf0' }, 'vrf0': { 'name': 'vrf0', 'id': 'vrf0', 'index': 3, 'interfaces': ['eth0'], } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'vrf0' interface2.netdef_id = 'vrf0' system_state.interface_list = [interface1, interface2] diff = NetplanDiffState(system_state, netplan_state) diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_vrf_link') self.assertIsNone(missing) missing = diff_data.get('interfaces', {}).get('vrf0', {}).get('netplan_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, []) def test_diff_missing_netplan_vrf_link(self): with open(self.path, "w") as f: f.write('''network: ethernets: eth0: {} eth1: {} vrfs: vrf0: table: 1000 interfaces: - eth1''') netplan_state = NetplanConfigState(rootdir=self.workdir.name) system_state = Mock(spec=SystemConfigState) system_state.get_data.return_value = { 'netplan-global-state': {}, 'eth0': { 'name': 'eth0', 'id': 'eth0', 'index': 2, 'vrf': 'vrf0', }, 'eth1': { 'name': 'eth1', 'id': 'eth1', 'index': 3, 'vrf': 'vrf0', }, 'vrf0': { 'name': 'vrf0', 'id': 'vrf0', 'index': 4, 'interfaces': ['eth0', 'eth1'] } } interface1 = Mock(spec=Interface) interface1.name = 'eth0' interface1.netdef_id = 'eth0' interface2 = Mock(spec=Interface) interface2.name = 'eth1' interface2.netdef_id = 'eth1' interface3 = Mock(spec=Interface) interface3.name = 'vrf0' interface3.netdef_id = 'vrf0' system_state.interface_list = [interface1, interface2, interface3] diff = NetplanDiffState(system_state, netplan_state) diff.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} diff_data = diff.get_diff() missing = diff_data.get('interfaces', {}).get('eth0', {}).get('netplan_state', {}).get('missing_vrf_link') self.assertEqual(missing, 'vrf0') missing = diff_data.get('interfaces', {}).get('vrf0', {}).get('netplan_state', {}).get('missing_interfaces', []) self.assertListEqual(missing, ['eth0']) netplan-1.0/tests/cli/test_status.py000066400000000000000000001220421457004145200176630ustar00rootroot00000000000000#!/usr/bin/python3 # Closed-box tests of netplan CLI. These are run during "make check" and don't # touch the system configuration at all. # # Copyright (C) 2022 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import io import unittest import yaml from contextlib import redirect_stdout from unittest.mock import patch from netplan_cli.cli.commands.status import NetplanStatus from netplan_cli.cli.state import Interface, SystemConfigState from tests.test_utils import call_cli IPROUTE2 = '[{"ifindex":1,"ifname":"lo","flags":["LOOPBACK","UP","LOWER_UP"],"mtu":65536,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"loopback","address":"00:00:00:00:00:00","broadcast":"00:00:00:00:00:00","promiscuity":0,"min_mtu":0,"max_mtu":0,"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"addr_info":[{"family":"inet","local":"127.0.0.1","prefixlen":8,"scope":"host","label":"lo","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"::1","prefixlen":128,"scope":"host","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":2,"ifname":"enp0s31f6","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"fq_codel","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"54:e1:ad:5f:24:b4","broadcast":"ff:ff:ff:ff:ff:ff","promiscuity":0,"min_mtu":68,"max_mtu":9000,"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"parentbus":"pci","parentdev":"0000:00:1f.6","addr_info":[{"family":"inet","local":"192.168.178.62","prefixlen":24,"metric":100,"broadcast":"192.168.178.255","scope":"global","dynamic":true,"label":"enp0s31f6","valid_life_time":850698,"preferred_life_time":850698},{"family":"inet6","local":"2001:9e8:a19f:1c00:56e1:adff:fe5f:24b4","prefixlen":64,"scope":"global","dynamic":true,"mngtmpaddr":true,"noprefixroute":true,"valid_life_time":6821,"preferred_life_time":3221},{"family":"inet6","local":"fe80::56e1:adff:fe5f:24b4","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":5,"ifname":"wlan0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"noqueue","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"1c:4d:70:e4:e4:0e","broadcast":"ff:ff:ff:ff:ff:ff","promiscuity":0,"min_mtu":256,"max_mtu":2304,"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"parentbus":"pci","parentdev":"0000:04:00.0","addr_info":[{"family":"inet","local":"192.168.178.142","prefixlen":24,"broadcast":"192.168.178.255","scope":"global","dynamic":true,"noprefixroute":true,"label":"wlan0","valid_life_time":850700,"preferred_life_time":850700},{"family":"inet6","local":"2001:9e8:a19f:1c00:7011:2d1:951:ad03","prefixlen":64,"scope":"global","temporary":true,"dynamic":true,"valid_life_time":6822,"preferred_life_time":3222},{"family":"inet6","local":"2001:9e8:a19f:1c00:f24f:f724:5dd1:d0ad","prefixlen":64,"scope":"global","dynamic":true,"mngtmpaddr":true,"noprefixroute":true,"valid_life_time":6822,"preferred_life_time":3222},{"family":"inet6","local":"fe80::fec1:6ced:5268:b46c","prefixlen":64,"scope":"link","noprefixroute":true,"valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":41,"ifname":"wg0","flags":["POINTOPOINT","NOARP","UP","LOWER_UP"],"mtu":1420,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"none","promiscuity":0,"min_mtu":0,"max_mtu":2147483552,"linkinfo":{"info_kind":"wireguard"},"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"addr_info":[{"family":"inet","local":"10.10.0.2","prefixlen":24,"scope":"global","label":"wg0","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":46,"ifname":"wwan0","flags":["BROADCAST","MULTICAST","NOARP"],"mtu":1500,"qdisc":"noop","operstate":"DOWN","group":"default","txqlen":1000,"link_type":"ether","address":"a2:23:44:c4:4e:f8","broadcast":"ff:ff:ff:ff:ff:ff","promiscuity":0,"min_mtu":0,"max_mtu":2048,"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"parentbus":"usb","parentdev":"1-6:1.12","addr_info":[]},{"ifindex":48,"link":null,"ifname":"tun0","flags":["POINTOPOINT","NOARP","UP","LOWER_UP"],"mtu":1480,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"sit","address":"1.1.1.1","link_pointtopoint":true,"broadcast":"2.2.2.2","promiscuity":0,"min_mtu":1280,"max_mtu":65555,"linkinfo":{"info_kind":"sit","info_data":{"proto":"ip6ip","remote":"2.2.2.2","local":"1.1.1.1","ttl":0,"pmtudisc":true,"prefix":"2002::","prefixlen":16}},"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"addr_info":[{"family":"inet6","local":"2001:dead:beef::2","prefixlen":64,"scope":"global","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":49,"ifname":"tun1","flags":["POINTOPOINT","MULTICAST","NOARP","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UNKNOWN","link_type":"none","linkinfo":{"info_kind":"tun","info_data":{"type":"tun"}}}]' # nopep8 NETWORKD = '{"Interfaces":[{"Index":1,"Name":"lo","AlternativeNames":[],"Type":"loopback","Driver":null,"SetupState":"unmanaged","OperationalState":"carrier","CarrierState":"carrier","AddressState":"off","IPv4AddressState":"off","IPv6AddressState":"off","OnlineState":null,"LinkFile":null,"Path":null,"Vendor":null,"Model":null},{"Index":2,"Name":"enp0s31f6","AlternativeNames":[],"Type":"ether","Driver":"e1000e","SetupState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/run/systemd/network/10-netplan-enp0s31f6.network","LinkFile":"/usr/lib/systemd/network/99-default.link","Path":"pci-0000:00:1f.6","Vendor":"Intel Corporation","Model":"Ethernet Connection I219-LM"},{"Index":5,"Name":"wlan0","AlternativeNames":[],"Type":"wlan","Driver":"iwlwifi","SetupState":"unmanaged","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/run/systemd/network/10-netplan-wlan0.network","LinkFile":"/usr/lib/systemd/network/80-iwd.link","Path":"pci-0000:04:00.0","Vendor":"Intel Corporation","Model":"Wireless 8260 (Dual Band Wireless-AC 8260)"},{"Index":41,"Name":"wg0","AlternativeNames":[],"Type":"wireguard","Driver":null,"SetupState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"off","OnlineState":"online","NetworkFile":"/run/systemd/network/10-netplan-wg0.network","LinkFile":"/usr/lib/systemd/network/99-default.link","Path":null,"Vendor":null,"Model":null},{"Index":46,"Name":"wwan0","AlternativeNames":[],"Type":"wwan","Driver":"cdc_mbim","SetupState":"unmanaged","OperationalState":"off","CarrierState":"off","AddressState":"off","IPv4AddressState":"off","IPv6AddressState":"off","OnlineState":null,"LinkFile":"/usr/lib/systemd/network/73-usb-net-by-mac.link","Path":"pci-0000:00:14.0-usb-0:6:1.12","Vendor":"Sierra Wireless, Inc.","Model":"EM7455"},{"Index":48,"Name":"tun0","AlternativeNames":[],"Type":"sit","Driver":null,"SetupState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"off","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/run/systemd/network/10-netplan-tun0.network","LinkFile":"/usr/lib/systemd/network/99-default.link","Path":null,"Vendor":null,"Model":null}, {"Index":43,"Name":"mybr0","Type":"bridge","Driver":"bridge","OperationalState":"degraded","CarrierState":"carrier","AddressState":"degraded","IPv4AddressState":"degraded","IPv6AddressState":"degraded","OnlineState":null}, {"Index":45,"Name":"mybond0","Type":"bond","Driver":"bonding","OperationalState":"degraded","CarrierState":"carrier","AddressState":"degraded","IPv4AddressState":"degraded","IPv6AddressState":"degraded","OnlineState":null}, {"Index":47,"Name":"myvrf0","Type":"ether","Kind":"vrf","Driver":"vrf","OperationalState":"degraded","CarrierState":"carrier","AddressState":"degraded","IPv4AddressState":"degraded","IPv6AddressState":"degraded","OnlineState":null},{"Index":49,"Name":"tun1","Kind":"tun","Type":"none","Driver":"tun"}]}' # nopep8 NMCLI = 'wlan0:MYCON:b6b7a21d-186e-45e1-b3a6-636da1735563:/run/NetworkManager/system-connections/netplan-NM-b6b7a21d-186e-45e1-b3a6-636da1735563-MYCON.nmconnection:802-11-wireless:yes' # nopep8 ROUTE4 = '[{"family":2,"type":"unicast","dst":"default","gateway":"192.168.178.1","dev":"enp0s31f6","table":"main","protocol":"dhcp","scope":"global","prefsrc":"192.168.178.62","metric":100,"flags":[]},{"family":2,"type":"unicast","dst":"default","gateway":"192.168.178.1","dev":"wlan0","table":"main","protocol":"dhcp","scope":"global","metric":600,"flags":[]},{"family":2,"type":"unicast","dst":"10.10.0.0/24","dev":"wg0","table":"main","protocol":"kernel","scope":"link","prefsrc":"10.10.0.2","flags":[]},{"family":2,"type":"unicast","dst":"192.168.178.0/24","dev":"enp0s31f6","table":"main","protocol":"kernel","scope":"link","prefsrc":"192.168.178.62","metric":100,"flags":[]},{"family":2,"type":"unicast","dst":"192.168.178.0/24","dev":"wlan0","table":"main","protocol":"kernel","scope":"link","prefsrc":"192.168.178.142","metric":600,"flags":[]},{"family":2,"type":"unicast","dst":"192.168.178.1","dev":"enp0s31f6","table":"1234","protocol":"dhcp","scope":"link","prefsrc":"192.168.178.62","metric":100,"flags":[]},{"family":2,"type":"broadcast","dst":"192.168.178.255","dev":"enp0s31f6","table":"local","protocol":"kernel","scope":"link","prefsrc":"192.168.178.62","flags":[]}]' # nopep8 ROUTE6 = '[{"family":10,"type":"unicast","dst":"::1","dev":"lo","table":"main","protocol":"kernel","scope":"global","metric":256,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/64","dev":"enp0s31f6","table":"main","protocol":"ra","scope":"global","metric":100,"flags":[],"expires":7199,"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/64","dev":"wlan0","table":"main","protocol":"ra","scope":"global","metric":600,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/56","gateway":"fe80::cece:1eff:fe3d:c737","dev":"enp0s31f6","table":"main","protocol":"ra","scope":"global","metric":100,"flags":[],"expires":1799,"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/56","gateway":"fe80::cece:1eff:fe3d:c737","dev":"wlan0","table":"main","protocol":"ra","scope":"global","metric":600,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:dead:beef::/64","dev":"tun0","table":"main","protocol":"kernel","scope":"global","metric":256,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"fe80::/64","dev":"enp0s31f6","table":"main","protocol":"kernel","scope":"global","metric":256,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"fe80::/64","dev":"wlan0","table":"main","protocol":"kernel","scope":"global","metric":1024,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"default","gateway":"fe80::cece:1eff:fe3d:c737","dev":"enp0s31f6","table":"1234","protocol":"ra","scope":"global","metric":100,"flags":[],"expires":1799,"metrics":[{"mtu":1492}],"pref":"medium"},{"family":10,"type":"unicast","dst":"default","gateway":"fe80::cece:1eff:fe3d:c737","dev":"wlan0","table":"main","protocol":"ra","scope":"global","metric":20600,"flags":[],"pref":"medium"}]' # nopep8 DNS_IP4 = bytearray([192, 168, 178, 1]) DNS_IP6 = bytearray([0xfd, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xce, 0xce, 0x1e, 0xff, 0xfe, 0x3d, 0xc7, 0x37]) DNS_ADDRESSES = [(5, 2, DNS_IP4), (5, 10, DNS_IP6), (2, 2, DNS_IP4), (2, 10, DNS_IP6)] # (IFidx, IPfamily, IPbytes) DNS_SEARCH = [(5, 'search.domain', False), (2, 'search.domain', False)] FAKE_DEV = {'ifindex': 42, 'ifname': 'fakedev0', 'flags': [], 'operstate': 'DOWN'} BRIDGE = {'ifindex': 43, 'ifname': 'mybr0', 'flags': [], 'operstate': 'UP'} BOND = {'ifindex': 45, 'ifname': 'mybond0', 'flags': [], 'operstate': 'UP'} VRF = {'ifindex': 47, 'ifname': 'myvrf0', 'flags': [], 'operstate': 'UP'} STATUS_OUTPUT = '''\ Online state: online DNS Addresses: 127.0.0.53 (stub) DNS Search: search.domain ● 2: enp0s31f6 ethernet UP (networkd: enp0s31f6) MAC Address: 54:e1:ad:5f:24:b4 (Intel Corporation) Addresses: 192.168.178.62/24 (dhcp) 2001:9e8:a19f:1c00:56e1:adff:fe5f:24b4/64 fe80::56e1:adff:fe5f:24b4/64 (link) DNS Addresses: 192.168.178.1 fd00::cece:1eff:fe3d:c737 DNS Search: search.domain Routes: default via 192.168.178.1 from 192.168.178.62 metric 100 (dhcp) 192.168.178.0/24 from 192.168.178.62 metric 100 (link) 2001:9e8:a19f:1c00::/64 metric 100 (ra) 2001:9e8:a19f:1c00::/56 via fe80::cece:1eff:fe3d:c737 metric 100 (ra) fe80::/64 metric 256 Activation Mode: manual ● 5: wlan0 wifi/"MYCON" UP (NetworkManager: NM-b6b7a21d-186e-45e1-b3a6-636da1735563) MAC Address: 1c:4d:70:e4:e4:0e (Intel Corporation) Addresses: 192.168.178.142/24 2001:9e8:a19f:1c00:7011:2d1:951:ad03/64 2001:9e8:a19f:1c00:f24f:f724:5dd1:d0ad/64 fe80::fec1:6ced:5268:b46c/64 (link) DNS Addresses: 192.168.178.1 fd00::cece:1eff:fe3d:c737 DNS Search: search.domain Routes: default via 192.168.178.1 metric 600 (dhcp) 192.168.178.0/24 from 192.168.178.142 metric 600 (link) 2001:9e8:a19f:1c00::/64 metric 600 (ra) 2001:9e8:a19f:1c00::/56 via fe80::cece:1eff:fe3d:c737 metric 600 (ra) fe80::/64 metric 1024 default via fe80::cece:1eff:fe3d:c737 metric 20600 (ra) ● 41: wg0 tunnel/wireguard UNKNOWN/UP (networkd: wg0) Addresses: 10.10.0.2/24 Routes: 10.10.0.0/24 from 10.10.0.2 (link) Activation Mode: manual ● 48: tun0 tunnel/sit UNKNOWN/UP (networkd: tun0) Addresses: 2001:dead:beef::2/64 Routes: 2001:dead:beef::/64 metric 256 Activation Mode: manual ● 42: fakedev0 other DOWN (unmanaged) Routes: 10.0.0.0/16 via 10.0.0.1 (local) ● 43: mybr0 bridge UP/DOWN (unmanaged) Interfaces: fakedev1 ● 44: fakedev1 other DOWN (unmanaged) Routes: 10.0.0.0/16 via 10.0.0.1 (local) Bridge: mybr0 ● 45: mybond0 bond UP/DOWN (unmanaged) Interfaces: fakedev2 ● 46: fakedev2 other DOWN (unmanaged) Routes: 10.0.0.0/16 via 10.0.0.1 (local) Bond: mybond0 ● 47: myvrf0 vrf UP/DOWN (unmanaged) Interfaces: fakedev3 ● 48: fakedev3 other DOWN (unmanaged) Routes: 10.0.0.0/16 via 10.0.0.1 (local) VRF: myvrf0 ● 49: tun1 tunnel/tun UNKNOWN/UP (unmanaged) 1 inactive interfaces hidden. Use "--all" to show all. ''' class TestStatus(unittest.TestCase): '''Test netplan status''' def setUp(self): self.maxDiff = None def _call(self, args): args.insert(0, 'status') return call_cli(args) def _get_itf(self, ifname): return next((itf for itf in yaml.safe_load(IPROUTE2) if itf['ifname'] == ifname), None) @patch('netplan_cli.cli.commands.status.RICH_OUTPUT', False) @patch('netplan_cli.cli.state.Interface.query_nm_ssid') @patch('netplan_cli.cli.state.Interface.query_networkctl') @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_plain_print(self, rt_mock, networkctl_mock, nm_ssid_mock): SSID = 'MYCON' nm_ssid_mock.return_value = SSID # networkctl mock output reduced to relevant lines networkctl_mock.return_value = \ '''Activation Policy: manual WiFi access point: {} (b4:fb:e4:75:c6:21)'''.format(SSID) rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} nd = SystemConfigState.process_networkd(NETWORKD) nm = SystemConfigState.process_nm(NMCLI) dns = (DNS_ADDRESSES, DNS_SEARCH) routes = (SystemConfigState.process_generic(ROUTE4), SystemConfigState.process_generic(ROUTE6)) fakeroute = {'type': 'local', 'dst': '10.0.0.0/16', 'gateway': '10.0.0.1', 'dev': FAKE_DEV['ifname'], 'table': 'main'} bridge = Interface(BRIDGE, nd, None, (None, None), (None, None)) bridge.members = ['fakedev1'] bridge_member = Interface(FAKE_DEV, [], None, (None, None), ([fakeroute], None)) bridge_member.idx = 44 bridge_member.name = 'fakedev1' bridge_member.bridge = 'mybr0' bond = Interface(BOND, nd, None, (None, None), (None, None)) bond.members = ['fakedev2'] bond_member = Interface(FAKE_DEV, [], None, (None, None), ([fakeroute], None)) bond_member.idx = 46 bond_member.name = 'fakedev2' bond_member.bond = 'mybond0' vrf = Interface(VRF, nd, None, (None, None), (None, None)) vrf.members = ['fakedev3'] vrf_member = Interface(FAKE_DEV, [], None, (None, None), ([fakeroute], None)) vrf_member.idx = 48 vrf_member.name = 'fakedev3' vrf_member.vrf = 'myvrf0' interfaces = [ Interface(self._get_itf('enp0s31f6'), nd, nm, dns, routes), Interface(self._get_itf('wlan0'), nd, nm, dns, routes), Interface(self._get_itf('wg0'), nd, nm, dns, routes), Interface(self._get_itf('tun0'), nd, nm, dns, routes), Interface(FAKE_DEV, [], None, (None, None), ([fakeroute], None)), bridge, bridge_member, bond, bond_member, vrf, vrf_member, Interface(self._get_itf('tun1'), nd, nm, dns, routes), ] data = {'netplan-global-state': { 'online': True, 'nameservers': { 'addresses': ['127.0.0.53'], 'search': ['search.domain'], 'mode': 'stub', }}} for itf in interfaces: ifname, obj = itf.json() data[ifname] = obj f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = False status.diff_only = False status.pretty_print(data, len(interfaces)+1, _console_width=130) out = f.getvalue() self.assertEqual(out, STATUS_OUTPUT) @patch('netplan_cli.cli.state.Interface.query_nm_ssid') @patch('netplan_cli.cli.state.Interface.query_networkctl') @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_pretty_print(self, rt_mock, networkctl_mock, nm_ssid_mock): SSID = 'MYCON' nm_ssid_mock.return_value = SSID # networkctl mock output reduced to relevant lines networkctl_mock.return_value = \ '''Activation Policy: manual WiFi access point: {} (b4:fb:e4:75:c6:21)'''.format(SSID) rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} nd = SystemConfigState.process_networkd(NETWORKD) nm = SystemConfigState.process_nm(NMCLI) dns = (DNS_ADDRESSES, DNS_SEARCH) routes = (SystemConfigState.process_generic(ROUTE4), SystemConfigState.process_generic(ROUTE6)) fakeroute = {'type': 'local', 'dst': '10.0.0.0/16', 'gateway': '10.0.0.1', 'dev': FAKE_DEV['ifname'], 'table': 'main'} bridge = Interface(BRIDGE, nd, None, (None, None), (None, None)) bridge.members = ['fakedev1'] bridge_member = Interface(FAKE_DEV, [], None, (None, None), ([fakeroute], None)) bridge_member.idx = 44 bridge_member.name = 'fakedev1' bridge_member.bridge = 'mybr0' bond = Interface(BOND, nd, None, (None, None), (None, None)) bond.members = ['fakedev2'] bond_member = Interface(FAKE_DEV, [], None, (None, None), ([fakeroute], None)) bond_member.idx = 46 bond_member.name = 'fakedev2' bond_member.bond = 'mybond0' vrf = Interface(VRF, nd, None, (None, None), (None, None)) vrf.members = ['fakedev3'] vrf_member = Interface(FAKE_DEV, [], None, (None, None), ([fakeroute], None)) vrf_member.idx = 48 vrf_member.name = 'fakedev3' vrf_member.vrf = 'myvrf0' interfaces = [ Interface(self._get_itf('enp0s31f6'), nd, nm, dns, routes), Interface(self._get_itf('wlan0'), nd, nm, dns, routes), Interface(self._get_itf('wg0'), nd, nm, dns, routes), Interface(self._get_itf('tun0'), nd, nm, dns, routes), Interface(FAKE_DEV, [], None, (None, None), ([fakeroute], None)), bridge, bridge_member, bond, bond_member, vrf, vrf_member, Interface(self._get_itf('tun1'), nd, nm, dns, routes), ] data = {'netplan-global-state': { 'online': True, 'nameservers': { 'addresses': ['127.0.0.53'], 'search': ['search.domain'], 'mode': 'stub', }}} for itf in interfaces: ifname, obj = itf.json() data[ifname] = obj f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = False status.diff_only = False status.pretty_print(data, len(interfaces)+1, _console_width=130) out = f.getvalue() self.assertEqual(out, STATUS_OUTPUT) @patch('netplan_cli.cli.state.Interface.query_nm_ssid') @patch('netplan_cli.cli.state.Interface.query_networkctl') @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_pretty_print_verbose(self, rt_mock, networkctl_mock, nm_ssid_mock): SSID = 'MYCON' nm_ssid_mock.return_value = SSID # networkctl mock output reduced to relevant lines networkctl_mock.return_value = \ '''Activation Policy: manual WiFi access point: {} (b4:fb:e4:75:c6:21)'''.format(SSID) rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} nd = SystemConfigState.process_networkd(NETWORKD) nm = SystemConfigState.process_nm(NMCLI) dns = (DNS_ADDRESSES, DNS_SEARCH) routes = (SystemConfigState.process_generic(ROUTE4), SystemConfigState.process_generic(ROUTE6)) interfaces = [ Interface(self._get_itf('enp0s31f6'), nd, nm, dns, routes), ] data = {'netplan-global-state': { 'online': True, 'nameservers': { 'addresses': ['127.0.0.53'], 'search': ['search.domain'], 'mode': 'stub', }}} for itf in interfaces: ifname, obj = itf.json() data[ifname] = obj f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = True status.diff = False status.diff_only = False status.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} status.pretty_print(data, len(interfaces), _console_width=130) out = f.getvalue() self.assertEqual(out, '''\ Online state: online DNS Addresses: 127.0.0.53 (stub) DNS Search: search.domain ● 2: enp0s31f6 ethernet UP (networkd: enp0s31f6) MAC Address: 54:e1:ad:5f:24:b4 (Intel Corporation) Addresses: 192.168.178.62/24 (dhcp) 2001:9e8:a19f:1c00:56e1:adff:fe5f:24b4/64 fe80::56e1:adff:fe5f:24b4/64 (link) DNS Addresses: 192.168.178.1 fd00::cece:1eff:fe3d:c737 DNS Search: search.domain Routes: default via 192.168.178.1 from 192.168.178.62 metric 100 table main (dhcp) 192.168.178.0/24 from 192.168.178.62 metric 100 table main (link) 192.168.178.1 from 192.168.178.62 metric 100 table 1234 (dhcp, link) 192.168.178.255 from 192.168.178.62 table local (link, broadcast) 2001:9e8:a19f:1c00::/64 metric 100 table main (ra) 2001:9e8:a19f:1c00::/56 via fe80::cece:1eff:fe3d:c737 metric 100 table main (ra) fe80::/64 metric 256 table main default via fe80::cece:1eff:fe3d:c737 metric 100 table 1234 (ra) Activation Mode: manual\n''') @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_call_cli(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, systemctl_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) out = self._call(['-a']) self.assertEqual(out.strip(), '''\ Online state: offline ● 42: fakedev0 other DOWN (unmanaged)''') @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') def test_fail_cli(self, networkd_mock, iproute2_mock, systemctl_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV] networkd_mock.return_value = [] with self.assertRaises(SystemExit): self._call([]) @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_call_cli_ifname(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, systemctl_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV, self._get_itf('wlan0')] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) out = self._call([FAKE_DEV['ifname']]) self.assertEqual(out.strip(), '''\ Online state: offline ● 42: fakedev0 other DOWN (unmanaged) 1 inactive interfaces hidden. Use "--all" to show all.''') @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_fail_cli_ifname(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, systemctl_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV, self._get_itf('wlan0')] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) with self.assertRaises(SystemExit): self._call(['notaninteface0']) @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_call_cli_json(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, systemctl_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) out = self._call(['-a', '--format=json']) self.assertEqual(out, '''{\ "netplan-global-state": {"online": false, "nameservers": {"addresses": [], "search": [], "mode": null}}, \ "fakedev0": {"index": 42, "adminstate": "DOWN", "operstate": "DOWN"}}\n''') @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_call_cli_yaml(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, systemctl_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) out = self._call(['-a', '--format=yaml']) self.assertEqual(out.strip(), '''\ fakedev0: adminstate: DOWN index: 42 operstate: DOWN netplan-global-state: nameservers: addresses: [] mode: null search: [] online: false'''.strip()) @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') @patch('netplan_cli.cli.utils.systemctl_is_active') @patch('netplan_cli.cli.utils.systemctl') def test_call_cli_no_networkd(self, systemctl_mock, is_active_mock, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock): iproute2_mock.return_value = [FAKE_DEV] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False is_active_mock.return_value = False state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) with self.assertLogs(level='DEBUG') as cm: self._call([]) self.assertIn('DEBUG:root:systemd-networkd.service is not active. Starting...', cm.output[0]) systemctl_mock.assert_called_with('start', ['systemd-networkd.service'], True) @patch('netplan_cli.cli.utils.systemctl_is_active') @patch('netplan_cli.cli.utils.systemctl_is_masked') def test_call_cli_networkd_masked(self, is_masked_mock, is_active_mock): is_active_mock.return_value = False is_masked_mock.return_value = True with self.assertLogs() as cm, self.assertRaises(SystemExit) as e: self._call([]) self.assertEqual(1, e.exception.code) self.assertIn('systemd-networkd.service is masked', cm.output[0]) @patch('netplan_cli.cli.state.NetplanConfigState.__init__') @patch('netplan_cli.cli.state_diff.NetplanDiffState.__init__') @patch('netplan_cli.cli.state_diff.NetplanDiffState.get_diff') @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_call_cli_diff_shallow(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, systemctl_mock, diff_state_get_diff_mock, diff_state_init_mock, state_init_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False state_init_mock.return_value = None diff_state_init_mock.return_value = None diff_state_get_diff_mock.return_value = {} state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) out = self._call(['--diff']) self.assertIn('Use "--diff-only" to omit the information that is consistent', out) @patch('netplan_cli.cli.state.NetplanConfigState.__init__') @patch('netplan_cli.cli.state_diff.NetplanDiffState.__init__') @patch('netplan_cli.cli.state_diff.NetplanDiffState.get_diff') @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_call_cli_diff_only_shallow(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, systemctl_mock, diff_state_get_diff_mock, diff_state_init_mock, state_init_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False state_init_mock.return_value = None diff_state_init_mock.return_value = None diff_state_get_diff_mock.return_value = {} state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) out = self._call(['--diff-only']) self.assertEqual('', out) @patch('netplan_cli.cli.state.NetplanConfigState.__init__') @patch('netplan_cli.cli.state_diff.NetplanDiffState.__init__') @patch('netplan_cli.cli.state_diff.NetplanDiffState.get_diff') @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_call_cli_diff_json_shallow(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, systemctl_mock, diff_state_get_diff_mock, diff_state_init_mock, state_init_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False state_init_mock.return_value = None diff_state_init_mock.return_value = None diff_state_get_diff_mock.return_value = {} state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) out = self._call(['--diff', '--format=json']) self.assertIn('{}', out) @patch('netplan_cli.cli.state.NetplanConfigState.__init__') @patch('netplan_cli.cli.state_diff.NetplanDiffState.__init__') @patch('netplan_cli.cli.state_diff.NetplanDiffState.get_diff') @patch('netplan_cli.cli.utils.systemctl') @patch('netplan_cli.cli.state.SystemConfigState.query_iproute2') @patch('netplan_cli.cli.state.SystemConfigState.query_networkd') @patch('netplan_cli.cli.state.SystemConfigState.query_nm') @patch('netplan_cli.cli.state.SystemConfigState.query_routes') @patch('netplan_cli.cli.state.SystemConfigState.query_resolved') @patch('netplan_cli.cli.state.SystemConfigState.resolvconf_json') @patch('netplan_cli.cli.state.SystemConfigState.query_online_state') def test_call_cli_diff_yaml_shallow(self, online_mock, resolvconf_mock, rd_mock, routes_mock, nm_mock, networkd_mock, iproute2_mock, systemctl_mock, diff_state_get_diff_mock, diff_state_init_mock, state_init_mock): systemctl_mock.return_value = None iproute2_mock.return_value = [FAKE_DEV] nm_mock.return_value = [] routes_mock.return_value = (None, None) rd_mock.return_value = (None, None) resolvconf_mock.return_value = {'addresses': [], 'search': [], 'mode': None} online_mock.return_value = False state_init_mock.return_value = None diff_state_init_mock.return_value = None diff_state_get_diff_mock.return_value = {} state = SystemConfigState() networkd_mock.return_value = state.process_networkd(NETWORKD) out = self._call(['--diff', '--format=yaml']) self.assertIn('{}', out) netplan-1.0/tests/cli/test_status_diff.py000066400000000000000000003066231457004145200206640ustar00rootroot00000000000000#!/usr/bin/python3 # Closed-box tests of netplan CLI. These are run during "make check" and don't # touch the system configuration at all. # # Copyright (C) 2024 Canonical, Ltd. # Author: Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import io import unittest from contextlib import redirect_stdout from unittest.mock import patch from netplan_cli.cli.commands.status import NetplanStatus from netplan.netdef import NetplanRoute class TestStatusDiff(unittest.TestCase): '''Test netplan status --diff''' def setUp(self): self.maxDiff = None @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_only_loopback_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True self.assertFalse(status._is_missing_dhcp6_address('enp5s0')) self.assertFalse(status._is_missing_dhcp4_address('enp5s0')) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_only_loopback_diff_verbose(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'dm0': {'index': 4, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'dummy-device', 'backend': 'networkd', 'id': 'dm0', 'macaddress': 'd2:2d:29:f5:58:e2', 'addresses': [{'fe80::d02d:29ff:fef5:58e2': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': 'fe80::d02d:29ff:fef5:58e2', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'br0': {'index': 6, 'adminstate': 'UP', 'operstate': 'DOWN', 'type': 'bridge', 'backend': 'networkd', 'id': 'br0', 'macaddress': '36:07:3b:d5:56:44', 'addresses': [{'fe80::3407:3bff:fed5:5644': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': 'fe80::3407:3bff:fed5:5644', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}, 'dm0': {'index': 4, 'name': 'dm0', 'id': 'dm0', 'system_state': {}, 'netplan_state': {}}, 'br0': {'index': 6, 'name': 'br0', 'id': 'br0', 'system_state': {}, 'netplan_state': {}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: 127.0.0.0/8 from 127.0.0.1 table local (host, local) 127.0.0.1 from 127.0.0.1 table local (host, local) 127.255.255.255 from 127.0.0.1 table local (link, broadcast) ::1 metric 256 table main ::1 table local (local) ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 table main (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 table main (link) 10.86.126.1 from 10.86.126.148 metric 100 table main (dhcp, link) 10.86.126.148 from 10.86.126.148 table local (host, local) 10.86.126.255 from 10.86.126.148 table local (link, broadcast) ff00::/8 metric 256 table local (multicast) ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) MAC Address: d2:2d:29:f5:58:e2 Addresses: fe80::d02d:29ff:fef5:58e2/64 (link) Routes: fe80::/64 metric 256 table main fe80::d02d:29ff:fef5:58e2 table local (local) ff00::/8 metric 256 table local (multicast) ● 6: br0 bridge DOWN/UP (networkd: br0) MAC Address: 36:07:3b:d5:56:44 Addresses: fe80::3407:3bff:fed5:5644/64 (link) Routes: fe80::/64 metric 256 table main fe80::3407:3bff:fed5:5644 table local (local) ff00::/8 metric 256 table local (multicast) Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = True status.diff = True status.diff_only = False status.state_diff = state_diff status.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_macaddress_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {'missing_macaddress': 'aa:bb:cc:dd:ee:ff'}, 'netplan_state': {'missing_macaddress': '00:16:3e:71:d0:1f'}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) + MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) - aa:bb:cc:dd:ee:ff (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 2: enp5s0 ethernet UP (networkd: enp5s0) + MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) - aa:bb:cc:dd:ee:ff (Red Hat, Inc.) ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_addresses_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}, {'1.2.3.4': {'prefix': 24}}, {'4.3.2.1': {'prefix': 24}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '1.2.3.0/24', 'family': 2, 'from': '1.2.3.4', 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '4.3.2.0/24', 'family': 2, 'from': '4.3.2.1', 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '1.2.3.4', 'family': 2, 'from': '1.2.3.4', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '1.2.3.255', 'family': 2, 'from': '1.2.3.4', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '4.3.2.1', 'family': 2, 'from': '4.3.2.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '4.3.2.255', 'family': 2, 'from': '4.3.2.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {'missing_addresses': ['10.20.30.40/24', '40.30.20.10/24']}, 'netplan_state': {'missing_addresses': ['4.3.2.1/24', '1.2.3.4/24']}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) + 1.2.3.4/24 + 4.3.2.1/24 - 10.20.30.40/24 - 40.30.20.10/24 DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 1.2.3.0/24 from 1.2.3.4 (link) 4.3.2.0/24 from 4.3.2.1 (link) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 2: enp5s0 ethernet UP (networkd: enp5s0) + Addresses: 1.2.3.4/24 + 4.3.2.1/24 - 10.20.30.40/24 - 40.30.20.10/24 ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_addresses_missing_dhcp(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': False, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {'missing_dhcp4_address': True, 'missing_dhcp6_address': True}, 'netplan_state': {}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) - Addresses: 0.0.0.0/0 (dhcp) - ::/0 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 2: enp5s0 ethernet UP (networkd: enp5s0) - Addresses: 0.0.0.0/0 (dhcp) - ::/0 (dhcp) ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_nameserver_addresses_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['1.1.1.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {'missing_nameservers_addresses': ['8.8.8.8']}, 'netplan_state': {'missing_nameservers_addresses': ['1.1.1.1']}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) + DNS Addresses: 1.1.1.1 - 8.8.8.8 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 2: enp5s0 ethernet UP (networkd: enp5s0) + DNS Addresses: 1.1.1.1 - 8.8.8.8 ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_nameserver_search_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['extradomain.home', 'test.local'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['extradomain.home', 'test.local'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {'missing_nameservers_search': ['somedomain.local']}, 'netplan_state': {'missing_nameservers_search': ['extradomain.home', 'test.local']}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 + DNS Search: extradomain.home + test.local - somedomain.local Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 2: enp5s0 ethernet UP (networkd: enp5s0) + DNS Search: extradomain.home + test.local - somedomain.local ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_routes_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '200.200.200.200', 'family': 2, 'via': '10.86.126.254', 'type': 'unicast', 'scope': 'global', 'protocol': 'boot', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {'missing_routes': [NetplanRoute(to='1.2.3.0/24', via='10.86.126.1', from_addr='4.3.2.1', type='unicast', scope='global', protocol=None, table=254, family=2, metric=4294967295, mtubytes=0, congestion_window=0, advertised_receive_window=0, onlink=0)]}, 'netplan_state': {'missing_routes': [NetplanRoute(to='200.200.200.200', via='10.86.126.254', from_addr=None, type='unicast', scope='global', protocol='boot', table=254, family=2, metric=4294967295, mtubytes=0, congestion_window=0, advertised_receive_window=0, onlink=False)]}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) + 200.200.200.200 via 10.86.126.254 (boot) - 1.2.3.0/24 via 10.86.126.1 from 4.3.2.1 Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 2: enp5s0 ethernet UP (networkd: enp5s0) + Routes: 200.200.200.200 via 10.86.126.254 (boot) - 1.2.3.0/24 via 10.86.126.1 from 4.3.2.1 ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_missing_system_routes_diff_verbose(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {'missing_routes': [NetplanRoute(to='100.200.200.0/0', via='1.1.2.2', from_addr=None, type='unicast', scope='global', protocol=None, table=254, family=2, metric=123, mtubytes=0, congestion_window=0, advertised_receive_window=0, onlink=0), NetplanRoute(to='1.2.3.0/24', via='1.1.2.2', from_addr=None, type='local', scope='host', protocol=None, table=254, family=2, metric=1000, mtubytes=0, congestion_window=0, advertised_receive_window=0, onlink=0)]}, 'netplan_state': {}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: 127.0.0.0/8 from 127.0.0.1 table local (host, local) 127.0.0.1 from 127.0.0.1 table local (host, local) 127.255.255.255 from 127.0.0.1 table local (link, broadcast) ::1 metric 256 table main ::1 table local (local) ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 table main (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 table main (link) 10.86.126.1 from 10.86.126.148 metric 100 table main (dhcp, link) 10.86.126.148 from 10.86.126.148 table local (host, local) 10.86.126.255 from 10.86.126.148 table local (link, broadcast) ff00::/8 metric 256 table local (multicast) - 100.200.200.0/0 via 1.1.2.2 metric 123 table main - 1.2.3.0/24 via 1.1.2.2 metric 1000 table main (host, local) Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = True status.diff = True status.diff_only = False status.state_diff = state_diff status.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 2: enp5s0 ethernet UP (networkd: enp5s0) - Routes: 100.200.200.0/0 via 1.1.2.2 metric 123 table main - 1.2.3.0/24 via 1.1.2.2 metric 1000 table main (host, local) ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = True status.diff = True status.diff_only = True status.state_diff = state_diff status.route_lookup_table_names = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_with_bridge_no_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'br0': {'index': 3, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'bridge', 'backend': 'networkd', 'id': 'br0', 'macaddress': '36:07:3b:d5:56:44', 'addresses': [{'fe80::3407:3bff:fed5:5644': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': 'fe80::3407:3bff:fed5:5644', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}], 'interfaces': ['dm0']}, 'dm0': {'index': 4, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'dummy-device', 'backend': 'networkd', 'id': 'dm0', 'macaddress': 'd2:2d:29:f5:58:e2', 'routes': [{'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}], 'bridge': 'br0'}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}, 'br0': {'index': 3, 'name': 'br0', 'id': 'br0', 'system_state': {'missing_addresses': ['192.168.5.1/24']}, 'netplan_state': {}}, 'dm0': {'index': 4, 'name': 'dm0', 'id': 'dm0', 'system_state': {}, 'netplan_state': {}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) ● 3: br0 bridge UP (networkd: br0) MAC Address: 36:07:3b:d5:56:44 Addresses: fe80::3407:3bff:fed5:5644/64 (link) - 192.168.5.1/24 Routes: fe80::/64 metric 256 Interfaces: dm0 ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) MAC Address: d2:2d:29:f5:58:e2 Bridge: br0 Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 3: br0 bridge UP (networkd: br0) - Addresses: 192.168.5.1/24 ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_bridge_interfaces_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'br0': {'index': 3, 'adminstate': 'UP', 'operstate': 'DOWN', 'type': 'bridge', 'backend': 'networkd', 'id': 'br0', 'macaddress': '36:07:3b:d5:56:44', 'addresses': [{'fe80::3407:3bff:fed5:5644': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': 'fe80::3407:3bff:fed5:5644', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}], 'interfaces': ['dm1']}, 'dm0': {'index': 4, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'dummy-device', 'backend': 'networkd', 'id': 'dm0', 'macaddress': 'd2:2d:29:f5:58:e2', 'routes': [{'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'dm1': {'index': 5, 'adminstate': 'DOWN', 'operstate': 'DOWN', 'type': 'dummy-device', 'macaddress': '16:dd:cc:b7:58:fa', 'bridge': 'br0'}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}, 'br0': {'index': 3, 'name': 'br0', 'id': 'br0', 'system_state': {'missing_interfaces': ['dm0']}, 'netplan_state': {'missing_interfaces': ['dm1']}}, 'dm0': {'index': 4, 'name': 'dm0', 'id': 'dm0', 'system_state': {'missing_bridge_link': 'br0'}, 'netplan_state': {}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'dm1': {'type': 'dummy-device', 'index': 5}, 'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) ● 3: br0 bridge DOWN/UP (networkd: br0) MAC Address: 36:07:3b:d5:56:44 Addresses: fe80::3407:3bff:fed5:5644/64 (link) Routes: fe80::/64 metric 256 + Interfaces: dm1 - dm0 ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) MAC Address: d2:2d:29:f5:58:e2 - Bridge: br0 + ● 5: dm1 dummy-device DOWN (unmanaged) MAC Address: 16:dd:cc:b7:58:fa Bridge: br0 Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 3: br0 bridge DOWN/UP (networkd: br0) + Interfaces: dm1 - dm0 ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) - Bridge: br0 + ● 5: dm1 dummy-device DOWN (unmanaged) ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_bridge_interfaces_missing_netplan_link(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'dm0': {'index': 4, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'dummy-device', 'backend': 'networkd', 'id': 'dm0', 'macaddress': 'd2:2d:29:f5:58:e2', 'addresses': [{'fe80::d02d:29ff:fef5:58e2': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': 'fe80::d02d:29ff:fef5:58e2', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}], 'bridge': 'br0'}, 'br0': {'index': 6, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'bridge', 'backend': 'networkd', 'id': 'br0', 'macaddress': '36:07:3b:d5:56:44', 'addresses': [{'fe80::3407:3bff:fed5:5644': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': 'fe80::3407:3bff:fed5:5644', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}], 'interfaces': ['dm0']}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}, 'dm0': {'index': 4, 'name': 'dm0', 'id': 'dm0', 'system_state': {}, 'netplan_state': {'missing_bridge_link': 'br0'}}, 'br0': {'index': 6, 'name': 'br0', 'id': 'br0', 'system_state': {}, 'netplan_state': {'missing_interfaces': ['dm0']}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) MAC Address: d2:2d:29:f5:58:e2 Addresses: fe80::d02d:29ff:fef5:58e2/64 (link) Routes: fe80::/64 metric 256 + Bridge: br0 ● 6: br0 bridge UP (networkd: br0) MAC Address: 36:07:3b:d5:56:44 Addresses: fe80::3407:3bff:fed5:5644/64 (link) Routes: fe80::/64 metric 256 + Interfaces: dm0 Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) + Bridge: br0 ● 6: br0 bridge UP (networkd: br0) + Interfaces: dm0 ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_bond_interfaces_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'bond0': {'index': 3, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'bond', 'backend': 'networkd', 'id': 'bond0', 'macaddress': '1a:b2:f0:35:da:4f', 'addresses': [{'fe80::18b2:f0ff:fe35:da4f': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': 'fe80::18b2:f0ff:fe35:da4f', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}], 'interfaces': ['dm1']}, 'dm0': {'index': 4, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'dummy-device', 'backend': 'networkd', 'id': 'dm0', 'macaddress': 'd2:2d:29:f5:58:e2', 'routes': [{'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'dm1': {'index': 6, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'dummy-device', 'macaddress': '1a:b2:f0:35:da:4f', 'bond': 'bond0'}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}, 'bond0': {'index': 3, 'name': 'bond0', 'id': 'bond0', 'system_state': {'missing_interfaces': ['dm0']}, 'netplan_state': {'missing_interfaces': ['dm1']}}, 'dm0': {'index': 4, 'name': 'dm0', 'id': 'dm0', 'system_state': {'missing_bond_link': 'bond0'}, 'netplan_state': {}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'dm1': {'type': 'dummy-device', 'index': 6}, 'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) ● 3: bond0 bond UP (networkd: bond0) MAC Address: 1a:b2:f0:35:da:4f Addresses: fe80::18b2:f0ff:fe35:da4f/64 (link) Routes: fe80::/64 metric 256 + Interfaces: dm1 - dm0 ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) MAC Address: d2:2d:29:f5:58:e2 - Bond: bond0 + ● 6: dm1 dummy-device UNKNOWN/UP (unmanaged) MAC Address: 1a:b2:f0:35:da:4f Bond: bond0 Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 3: bond0 bond UP (networkd: bond0) + Interfaces: dm1 - dm0 ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) - Bond: bond0 + ● 6: dm1 dummy-device UNKNOWN/UP (unmanaged) ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_bond_interfaces_missing_netplan_link(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'bond0': {'index': 3, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'bond', 'backend': 'networkd', 'id': 'bond0', 'macaddress': '1a:b2:f0:35:da:4f', 'addresses': [{'fe80::18b2:f0ff:fe35:da4f': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': 'fe80::18b2:f0ff:fe35:da4f', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}], 'interfaces': ['dm0']}, 'dm0': {'index': 4, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'dummy-device', 'backend': 'networkd', 'id': 'dm0', 'macaddress': '1a:b2:f0:35:da:4f', 'bond': 'bond0'}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}, 'bond0': {'index': 3, 'name': 'bond0', 'id': 'bond0', 'system_state': {}, 'netplan_state': {'missing_interfaces': ['dm0']}}, 'dm0': {'index': 4, 'name': 'dm0', 'id': 'dm0', 'system_state': {}, 'netplan_state': {'missing_bond_link': 'bond0'}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) ● 3: bond0 bond UP (networkd: bond0) MAC Address: 1a:b2:f0:35:da:4f Addresses: fe80::18b2:f0ff:fe35:da4f/64 (link) Routes: fe80::/64 metric 256 + Interfaces: dm0 ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) MAC Address: 1a:b2:f0:35:da:4f + Bond: bond0 Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 3: bond0 bond UP (networkd: bond0) + Interfaces: dm0 ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) + Bond: bond0 ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_vrf_interfaces_diff(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'dm0': {'index': 3, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'dummy-device', 'backend': 'networkd', 'id': 'dm0', 'macaddress': 'd2:2d:29:f5:58:e2', 'addresses': [{'fe80::d02d:29ff:fef5:58e2': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': 'fe80::d02d:29ff:fef5:58e2', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'vrf0': {'index': 4, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'vrf', 'backend': 'networkd', 'id': 'vrf0', 'macaddress': '72:1e:4b:7d:ac:5f', 'interfaces': ['dm1']}, 'dm1': {'index': 5, 'adminstate': 'DOWN', 'operstate': 'DOWN', 'type': 'dummy-device', 'macaddress': '16:dd:cc:b7:58:fa', 'vrf': 'vrf0'}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}, 'dm0': {'index': 3, 'name': 'dm0', 'id': 'dm0', 'system_state': {'missing_vrf_link': 'vrf0'}, 'netplan_state': {}}, 'vrf0': {'index': 4, 'name': 'vrf0', 'id': 'vrf0', 'system_state': {'missing_interfaces': ['dm0']}, 'netplan_state': {'missing_interfaces': ['dm1']}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'dm1': {'type': 'dummy-device', 'index': 5}, 'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) ● 3: dm0 dummy-device UNKNOWN/UP (networkd: dm0) MAC Address: d2:2d:29:f5:58:e2 Addresses: fe80::d02d:29ff:fef5:58e2/64 (link) Routes: fe80::/64 metric 256 - VRF: vrf0 ● 4: vrf0 vrf UP (networkd: vrf0) MAC Address: 72:1e:4b:7d:ac:5f + Interfaces: dm1 - dm0 + ● 5: dm1 dummy-device DOWN (unmanaged) MAC Address: 16:dd:cc:b7:58:fa VRF: vrf0 Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 3: dm0 dummy-device UNKNOWN/UP (networkd: dm0) - VRF: vrf0 ● 4: vrf0 vrf UP (networkd: vrf0) + Interfaces: dm1 - dm0 + ● 5: dm1 dummy-device DOWN (unmanaged) ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_vrf_interfaces_missing_netplan_link(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'dm0': {'index': 4, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'dummy-device', 'backend': 'networkd', 'id': 'dm0', 'macaddress': 'd2:2d:29:f5:58:e2', 'addresses': [{'fe80::d02d:29ff:fef5:58e2': {'prefix': 64, 'flags': ['link']}}], 'routes': [{'to': 'fe80::d02d:29ff:fef5:58e2', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': '1234'}, {'to': 'fe80::/64', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': '1234'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': '1234'}], 'vrf': 'vrf0'}, 'vrf0': {'index': 5, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'vrf', 'backend': 'networkd', 'id': 'vrf0', 'macaddress': '72:1e:4b:7d:ac:5f', 'interfaces': ['dm0']}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}, 'dm0': {'index': 4, 'name': 'dm0', 'id': 'dm0', 'system_state': {}, 'netplan_state': {'missing_vrf_link': 'vrf0'}}, 'vrf0': {'index': 5, 'name': 'vrf0', 'id': 'vrf0', 'system_state': {}, 'netplan_state': {'missing_interfaces': ['dm0']}}}, 'missing_interfaces_system': {}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) MAC Address: d2:2d:29:f5:58:e2 Addresses: fe80::d02d:29ff:fef5:58e2/64 (link) + VRF: vrf0 ● 5: vrf0 vrf UP (networkd: vrf0) MAC Address: 72:1e:4b:7d:ac:5f + Interfaces: dm0 Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) ● 4: dm0 dummy-device UNKNOWN/UP (networkd: dm0) + VRF: vrf0 ● 5: vrf0 vrf UP (networkd: vrf0) + Interfaces: dm0 ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_missing_system_interfaces(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}}, 'missing_interfaces_system': {'eth0': {'type': 'ethernet'}, 'eth1': {'type': 'ethernet'}}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) MAC Address: 00:00:00:00:00:00 Addresses: 127.0.0.1/8 ::1/128 Routes: ::1 metric 256 ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) - ● eth0 ethernet - ● eth1 ethernet Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '''+ ● 1: lo ethernet UNKNOWN/UP (unmanaged) - ● eth0 ethernet - ● eth1 ethernet ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = None status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) status.state_diff = None self.assertDictEqual(status._get_missing_system_interfaces(), {}) @patch('netplan_cli.cli.state_diff.route_table_lookup') def test_with_targeted_interfaces(self, rt_mock): rt_mock.return_value = { 0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} input_data = {'netplan-global-state': {'online': True, 'nameservers': {'addresses': ['127.0.0.53'], 'search': ['lxd'], 'mode': 'stub'}}, 'lo': {'index': 1, 'adminstate': 'UP', 'operstate': 'UNKNOWN', 'type': 'ethernet', 'macaddress': '00:00:00:00:00:00', 'addresses': [{'127.0.0.1': {'prefix': 8}}, {'::1': {'prefix': 128}}], 'routes': [{'to': '127.0.0.0/8', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.0.0.1', 'family': 2, 'from': '127.0.0.1', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '127.255.255.255', 'family': 2, 'from': '127.0.0.1', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': '::1', 'family': 10, 'metric': 256, 'type': 'unicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'main'}, {'to': '::1', 'family': 10, 'type': 'local', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}, 'enp5s0': {'index': 2, 'adminstate': 'UP', 'operstate': 'UP', 'type': 'ethernet', 'backend': 'networkd', 'id': 'enp5s0', 'macaddress': '00:16:3e:71:d0:1f', 'vendor': 'Red Hat, Inc.', 'addresses': [{'10.86.126.148': {'prefix': 24, 'flags': ['dhcp']}}], 'dns_addresses': ['10.86.126.1'], 'dns_search': ['lxd'], 'routes': [{'to': 'default', 'family': 2, 'via': '10.86.126.1', 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'global', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.0/24', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'kernel', 'table': 'main'}, {'to': '10.86.126.1', 'family': 2, 'from': '10.86.126.148', 'metric': 100, 'type': 'unicast', 'scope': 'link', 'protocol': 'dhcp', 'table': 'main'}, {'to': '10.86.126.148', 'family': 2, 'from': '10.86.126.148', 'type': 'local', 'scope': 'host', 'protocol': 'kernel', 'table': 'local'}, {'to': '10.86.126.255', 'family': 2, 'from': '10.86.126.148', 'type': 'broadcast', 'scope': 'link', 'protocol': 'kernel', 'table': 'local'}, {'to': 'ff00::/8', 'family': 10, 'metric': 256, 'type': 'multicast', 'scope': 'global', 'protocol': 'kernel', 'table': 'local'}]}} # nopep8 state_diff = {'interfaces': {'enp5s0': {'index': 2, 'name': 'enp5s0', 'id': 'enp5s0', 'system_state': {}, 'netplan_state': {}}}, 'missing_interfaces_system': {'eth0': {'type': 'ethernet'}, 'eth1': {'type': 'ethernet'}}, 'missing_interfaces_netplan': {'lo': {'type': 'ethernet', 'index': 1}}} # nopep8 expected = ''' ● 2: enp5s0 ethernet UP (networkd: enp5s0) MAC Address: 00:16:3e:71:d0:1f (Red Hat, Inc.) Addresses: 10.86.126.148/24 (dhcp) DNS Addresses: 10.86.126.1 DNS Search: lxd Routes: default via 10.86.126.1 from 10.86.126.148 metric 100 (dhcp) 10.86.126.0/24 from 10.86.126.148 metric 100 (link) 10.86.126.1 from 10.86.126.148 metric 100 (dhcp, link) Use "--diff-only" to omit the information that is consistent between the system and Netplan. ''' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = 'enp5s0' status.verbose = False status.diff = True status.diff_only = False status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) # Same test for --diff-only expected = '' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = 'enp5s0' status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) status.state_diff = None self.assertDictEqual(status._get_missing_system_interfaces(), {}) # Same test with an interface missing in the system expected = '- ● eth0 ethernet\n' f = io.StringIO() with redirect_stdout(f): status = NetplanStatus() status.ifname = 'eth0' status.verbose = False status.diff = True status.diff_only = True status.state_diff = state_diff status.pretty_print(input_data, 2, _console_width=130) out = f.getvalue() self.assertEqual(out, expected) status.state_diff = None self.assertDictEqual(status._get_missing_system_interfaces(), {}) netplan-1.0/tests/cli/test_units.py000066400000000000000000000202071457004145200175020ustar00rootroot00000000000000#!/usr/bin/python3 # Functional tests of netplan CLI. These are run during "make check" and don't # touch the system configuration at all. # # Copyright (C) 2021 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import shutil import sys import unittest import subprocess import tempfile from unittest.mock import patch from netplan_cli.cli.commands.apply import NetplanApply from netplan_cli.cli.commands.try_command import NetplanTry from netplan_cli.cli.core import Netplan class TestCLI(unittest.TestCase): '''Netplan CLI unittests''' def setUp(self): self.tmproot = tempfile.mkdtemp() os.mkdir(os.path.join(self.tmproot, 'run')) os.makedirs(os.path.join(self.tmproot, 'etc/netplan')) os.environ['DBUS_TEST_NETPLAN_ROOT'] = self.tmproot def tearDown(self): shutil.rmtree(self.tmproot) def test_is_composite_member(self): res = NetplanApply.is_composite_member([{'br0': {'interfaces': ['eth0']}}], 'eth0') self.assertTrue(res) def test_is_composite_member_false(self): res = NetplanApply.is_composite_member([ {'br0': {'interfaces': ['eth42']}}, {'bond0': {'interfaces': ['eth1']}} ], 'eth0') self.assertFalse(res) def test_is_composite_member_with_renderer(self): res = NetplanApply.is_composite_member([{'renderer': 'networkd', 'br0': {'interfaces': ['eth0']}}], 'eth0') self.assertTrue(res) @patch('subprocess.check_call') def test_clear_virtual_links(self, mock): # simulate as if 'tun3' would have already been delete another way, # e.g. via NetworkManager backend res = NetplanApply.clear_virtual_links(['br0', 'vlan2', 'bond1', 'tun3'], ['br0', 'vlan2'], devices=['br0', 'vlan2', 'bond1', 'eth0']) mock.assert_called_with(['ip', 'link', 'delete', 'dev', 'bond1']) self.assertIn('bond1', res) self.assertIn('tun3', res) self.assertNotIn('br0', res) self.assertNotIn('vlan2', res) @patch('subprocess.check_call') def test_clear_virtual_links_failure(self, mock): mock.side_effect = subprocess.CalledProcessError(1, '', 'Cannot find device "br0"') res = NetplanApply.clear_virtual_links(['br0'], [], devices=['br0', 'eth0']) mock.assert_called_with(['ip', 'link', 'delete', 'dev', 'br0']) self.assertIn('br0', res) self.assertNotIn('eth0', res) @patch('subprocess.check_call') def test_clear_virtual_links_no_delta(self, mock): res = NetplanApply.clear_virtual_links(['br0', 'vlan2'], ['br0', 'vlan2'], devices=['br0', 'vlan2', 'eth0']) mock.assert_not_called() self.assertEqual(res, []) @patch('subprocess.check_call') def test_clear_virtual_links_no_devices(self, mock): with self.assertLogs('', level='INFO') as ctx: res = NetplanApply.clear_virtual_links(['br0', 'br1'], ['br0']) self.assertEqual(res, []) self.assertEqual(ctx.output, ['WARNING:root:Cannot clear virtual links: no network interfaces provided.']) mock.assert_not_called() def test_netplan_try_ready_stamp(self): stamp_file = os.path.join(self.tmproot, 'run', 'netplan', 'netplan-try.ready') cmd = NetplanTry() self.assertFalse(os.path.isfile(stamp_file)) # make sure it behaves correctly, if the file doesn't exist self.assertFalse(cmd.clear_ready_stamp()) self.assertFalse(os.path.isfile(stamp_file)) cmd.touch_ready_stamp() self.assertTrue(os.path.isfile(stamp_file)) self.assertTrue(cmd.clear_ready_stamp()) self.assertFalse(os.path.isfile(stamp_file)) def test_netplan_try_is_revertable(self): with open(os.path.join(self.tmproot, 'etc/netplan/test.yaml'), 'w') as f: f.write('''network: bridges: br54: dhcp4: false ''') cmd = NetplanTry() self.assertTrue(cmd.is_revertable()) def test_netplan_try_is_revertable_fail(self): extra_config = os.path.join(self.tmproot, 'extra.yaml') with open(extra_config, 'w') as f: f.write('''network: bridges: br54: INVALID: kaputt ''') cmd = NetplanTry() cmd.config_file = extra_config self.assertRaises(SystemExit, cmd.is_revertable) def test_netplan_try_is_not_revertable(self): with open(os.path.join(self.tmproot, 'etc/netplan/test.yaml'), 'w') as f: f.write('''network: ethernets: eth0: dhcp4: true bonds: bn0: interfaces: [eth0] parameters: mode: balance-rr ''') cmd = NetplanTry() self.assertFalse(cmd.is_revertable()) def test_raises_exception_main_function(self): with open(os.path.join(self.tmproot, 'etc/netplan/test.yaml'), 'w') as f: f.write('''network: ethernets: eth0: dhcp4: nothanks''') # The idea was to capture stderr here but for some reason # my attempts to mock sys.stderr didn't work with pytest # This will get the error message passed to logging.warning # as a parameter with patch('logging.warning') as log: old_argv = sys.argv args = ['get', '--root-dir', self.tmproot] sys.argv = [old_argv[0]] + args Netplan().main() sys.argv = old_argv args = log.call_args.args self.assertIn('Error in network definition: invalid boolean value', args[0]) @unittest.skipIf(os.getuid() == 0, 'Root can always read the file') def test_raises_exception_main_function_permission_denied(self): with open(os.path.join(self.tmproot, 'etc/netplan/test.yaml'), 'w') as f: f.write('''network: ethernets: eth0: dhcp4: nothanks''') os.chmod(os.path.join(self.tmproot, 'etc/netplan/test.yaml'), 0) with patch('logging.warning') as log: old_argv = sys.argv args = ['get', '--root-dir', self.tmproot] sys.argv = [old_argv[0]] + args Netplan().main() sys.argv = old_argv args = log.call_args.args self.assertIn('Permission denied', args[0]) def test_get_validation_error_exception(self): with open(os.path.join(self.tmproot, 'etc/netplan/test.yaml'), 'w') as f: f.write('''network: ethernets: eth0: set-name: abc''') with patch('logging.warning') as log: old_argv = sys.argv args = ['get', '--root-dir', self.tmproot] sys.argv = [old_argv[0]] + args Netplan().main() sys.argv = old_argv args = log.call_args.args self.assertIn('etc/netplan/test.yaml: Error in network definition', args[0]) def test_set_generic_validation_error_exception(self): with open(os.path.join(self.tmproot, 'etc/netplan/test.yaml'), 'w') as f: f.write('''network: vrfs: vrf0: table: 100 routes: - table: 200 to: 1.2.3.4''') with patch('logging.warning') as log: old_argv = sys.argv args = ['get', '--root-dir', self.tmproot] sys.argv = [old_argv[0]] + args Netplan().main() sys.argv = old_argv args = log.call_args.args self.assertIn("VRF routes table mismatch", args[0]) netplan-1.0/tests/cli_legacy.py000077500000000000000000001062431457004145200166350ustar00rootroot00000000000000#!/usr/bin/python3 # Functional tests of netplan CLI. These are run during "make check" and don't # touch the system configuration at all. # # Copyright (C) 2016 Canonical, Ltd. # Author: Martin Pitt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys import subprocess import unittest import tempfile import shutil import yaml from tests.test_utils import MockCmd rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) exe_cli = [os.path.join(rootdir, 'src', 'netplan.script')] if shutil.which('python3-coverage'): exe_cli = ['python3-coverage', 'run', '--append', '--'] + exe_cli os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))}) # combined the output of 'nmcli dev' and 'nmcli con' into a single mock output, # connected via FAKE_NM_ID. Both are parsed line by line, checking for # 'GENERAL.CONNECTION' or 'connection.id/uuid' respectively nmcli_mock_output = ''' # nmcli dev ... MOCK OUTPUT: GENERAL.CONNECTION: FAKE_NM_ID # nmcli con ... MOCK OUTPUT: connection.id: FAKE_NM_ID connection.uuid: 00000000-0000-0000-0000-000000000000 ''' def _load_yaml(text): return yaml.load(text, Loader=yaml.SafeLoader) class TestArgs(unittest.TestCase): '''Generic argument parsing tests''' def test_global_help(self): out = subprocess.check_output(exe_cli + ['--help']) self.assertIn(b'Available commands', out) self.assertIn(b'generate', out) self.assertIn(b'--debug', out) def test_command_help(self): out = subprocess.check_output(exe_cli + ['generate', '--help']) self.assertIn(b'--root-dir', out) def test_no_command(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) p = subprocess.Popen(exe_cli, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() self.assertEqual(out, b'') self.assertIn(b'need to specify a command', err) self.assertNotEqual(p.returncode, 0) class TestGenerate(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() def test_no_config(self): p = subprocess.Popen(exe_cli + ['generate', '--root-dir', self.workdir.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() self.assertEqual(out, b'') self.assertEqual(os.listdir(self.workdir.name), ['run']) def test_with_empty_config(self): c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) path_a = os.path.join(c, 'a.yaml') path_b = os.path.join(c, 'b.yaml') open(path_a, 'w').close() with open(path_b, 'w') as f: f.write('''network: version: 2 ethernets: enlol: {dhcp4: yes}''') os.chmod(path_a, mode=0o600) os.chmod(path_b, mode=0o600) out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name], stderr=subprocess.STDOUT) self.assertEqual(out, b'') self.assertEqual(os.listdir(os.path.join(self.workdir.name, 'run', 'systemd', 'network')), ['10-netplan-enlol.network']) def test_with_config(self): c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: enlol: {dhcp4: yes}''') out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name]) self.assertEqual(out, b'') self.assertEqual(os.listdir(os.path.join(self.workdir.name, 'run', 'systemd', 'network')), ['10-netplan-enlol.network']) def test_mapping_for_unknown_iface(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: enlol: {dhcp4: yes}''') p = subprocess.Popen(exe_cli + ['generate', '--root-dir', self.workdir.name, '--mapping', 'nonexistent'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() self.assertNotEqual(p.returncode, 0) self.assertNotIn(b'nonexistent', out) def test_mapping_for_interface(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: enlol: {dhcp4: yes}''') out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name, '--mapping', 'enlol']) self.assertNotEqual(b'', out) self.assertIn('enlol', out.decode('utf-8')) def test_mapping_for_renamed_iface(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: myif: match: name: enlol set-name: renamediface dhcp4: yes ''') out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name, '--mapping', 'renamediface']) self.assertNotEqual(b'', out) self.assertIn('renamediface', out.decode('utf-8')) class TestIfupdownMigrate(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() self.ifaces_path = os.path.join(self.workdir.name, 'etc/network/interfaces') self.converted_path = os.path.join(self.workdir.name, 'etc/netplan/10-ifupdown.yaml') def test_system(self): os.environ.update({"ENABLE_TEST_COMMANDS": "1"}) rc = subprocess.call(exe_cli + ['migrate', '--dry-run'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # may succeed or fail, but should not crash self.assertIn(rc, [0, 2]) def do_test(self, iface_file, expect_success=True, dry_run=True, dropins=None): os.environ.update({"ENABLE_TEST_COMMANDS": "1"}) if iface_file is not None: os.makedirs(os.path.dirname(self.ifaces_path)) with open(self.ifaces_path, 'w') as f: f.write(iface_file) if dropins: for fname, contents in dropins.items(): path = os.path.join(os.path.dirname(self.ifaces_path), fname) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, 'w') as f: f.write(contents) argv = exe_cli + ['--debug', 'migrate', '--root-dir', self.workdir.name] if dry_run: argv.append('--dry-run') p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() if expect_success: self.assertEqual(p.returncode, 0, err.decode()) else: self.assertIn(p.returncode, [2, 3], err.decode()) return (out, err) # # configs which can be converted # def test_no_config(self): (out, err) = self.do_test(None) self.assertEqual(out, b'') self.assertEqual(os.listdir(self.workdir.name), []) def test_only_empty_include(self): out = self.do_test('''# default interfaces file source-directory /etc/network/interfaces.d''')[0] self.assertFalse(os.path.exists(self.converted_path)) self.assertEqual(out, b'') def test_loopback_only(self): (out, err) = self.do_test('auto lo\n#ignore me\niface lo inet loopback') self.assertEqual(out, b'') self.assertIn(b'nothing to migrate\n', err) def test_dhcp4(self): out = self.do_test('auto en1\niface en1 inet dhcp')[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) def test_dhcp6(self): out = self.do_test('auto en1\niface en1 inet6 dhcp')[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp6': True}}}}, out.decode()) def test_dhcp4_and_6(self): out = self.do_test('auto lo\niface lo inet loopback\n\n' 'auto en1\niface en1 inet dhcp\niface en1 inet6 dhcp')[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp4': True, 'dhcp6': True}}}}, out.decode()) def test_includedir_rel(self): out = self.do_test('iface lo inet loopback\nauto lo\nsource-directory interfaces.d', dropins={'interfaces.d/std': 'auto en1\niface en1 inet dhcp', 'interfaces.d/std.bak': 'some_bogus dontreadme'})[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) def test_includedir_abs(self): out = self.do_test('iface lo inet loopback\nauto lo\nsource-directory /etc/network/defs/my', dropins={'defs/my/std': 'auto en1\niface en1 inet dhcp', 'defs/my/std.bak': 'some_bogus dontreadme'})[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) def test_include_rel(self): out = self.do_test('iface lo inet loopback\nauto lo\nsource interfaces.d/*.cfg', dropins={'interfaces.d/std.cfg': 'auto en1\niface en1 inet dhcp', 'interfaces.d/std.cfgold': 'some_bogus dontreadme'})[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) def test_include_abs(self): out = self.do_test('iface lo inet loopback\nauto lo\nsource /etc/network/*.cfg', dropins={'std.cfg': 'auto en1\niface en1 inet dhcp', 'std.cfgold': 'some_bogus dontreadme'})[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) def test_allow(self): out = self.do_test('allow-hotplug en1\niface en1 inet dhcp\n' 'allow-auto en2\niface en2 inet dhcp')[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp4': True}, 'en2': {'dhcp4': True}}}}, out.decode()) def test_no_scripts(self): out = self.do_test('auto en1\niface en1 inet dhcp\nno-scripts en1')[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) def test_write_file_noconfig(self): (out, err) = self.do_test('auto lo\niface lo inet loopback', dry_run=False) self.assertFalse(os.path.exists(self.converted_path)) # should disable original ifupdown config self.assertFalse(os.path.exists(self.ifaces_path)) self.assertTrue(os.path.exists(self.ifaces_path + '.netplan-converted')) def test_write_file_haveconfig(self): (out, err) = self.do_test('auto en1\niface en1 inet dhcp', dry_run=False) with open(self.converted_path) as f: config = _load_yaml(f) self.assertEqual(config, {'network': { 'version': 2, 'ethernets': {'en1': {'dhcp4': True}}}}) # should disable original ifupdown config self.assertFalse(os.path.exists(self.ifaces_path)) self.assertTrue(os.path.exists(self.ifaces_path + '.netplan-converted')) def test_write_file_prev_run(self): os.makedirs(os.path.dirname(self.converted_path)) with open(self.converted_path, 'w') as f: f.write('canary') (out, err) = self.do_test('auto en1\niface en1 inet dhcp', dry_run=False, expect_success=False) with open(self.converted_path) as f: self.assertEqual(f.read(), 'canary') # should not disable original ifupdown config self.assertTrue(os.path.exists(self.ifaces_path)) # # static # def test_static_ipv4_prefix(self): out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8', dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"]}}}}, out.decode()) def test_static_ipv4_netmask(self): out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4\nnetmask 255.0.0.0', dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"]}}}}, out.decode()) def test_static_ipv4_no_address(self): out, err = self.do_test('auto en1\niface en1 inet static\nnetmask 1.2.3.4', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'no address supplied', err) def test_static_ipv4_no_network(self): out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'does not specify prefix length, and netmask not specified', err) def test_static_ipv4_invalid_addr(self): out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.400/8', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'error parsing "1.2.3.400" as an IPv4 address', err) def test_static_ipv4_invalid_netmask(self): out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4\nnetmask 123.123.123.0', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'error parsing "1.2.3.4/123.123.123.0" as an IPv4 network', err) def test_static_ipv4_invalid_prefixlen(self): out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/42', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'error parsing "1.2.3.4/42" as an IPv4 network', err) def test_static_ipv4_unsupported_option(self): out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/24\nmetric 1280', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'unsupported inet option "metric"', err) def test_static_ipv4_unknown_option(self): out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/24\nxyzzy 1280', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'unknown inet option "xyzzy"', err) def test_static_ipv6_prefix(self): out = self.do_test('auto en1\niface en1 inet6 static\naddress fc00:0123:4567:89ab:cdef::1234/64', dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["fc00:123:4567:89ab:cdef::1234/64"]}}}}, out.decode()) def test_static_ipv6_netmask(self): out = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234\nnetmask 64', dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["fc00:123:4567:89ab:cdef::1234/64"]}}}}, out.decode()) def test_static_ipv6_no_address(self): out, err = self.do_test('auto en1\niface en1 inet6 static\nnetmask 64', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'no address supplied', err) def test_static_ipv6_no_network(self): out, err = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'does not specify prefix length, and netmask not specified', err) def test_static_ipv6_invalid_addr(self): out, err = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::12345/64', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'error parsing "fc00:0123:4567:89ab:cdef::12345" as an IPv6 address', err) def test_static_ipv6_invalid_netmask(self): out, err = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234\nnetmask 129', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'error parsing "fc00:0123:4567:89ab:cdef::1234/129" as an IPv6 network', err) def test_static_ipv6_invalid_prefixlen(self): out, err = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234/129', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'error parsing "fc00:0123:4567:89ab:cdef::1234/129" as an IPv6 network', err) def test_static_ipv6_unsupported_option(self): out, err = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234/64\nmetric 1280', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'unsupported inet6 option "metric"', err) def test_static_ipv6_unknown_option(self): out, err = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234/64\nxyzzy 1280', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'unknown inet6 option "xyzzy"', err) def test_static_ipv6_accept_ra_0(self): out = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234/64\naccept_ra 0', dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["fc00:123:4567:89ab:cdef::1234/64"], 'accept_ra': False}}}}, out.decode()) def test_static_ipv6_accept_ra_1(self): out = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234/64\naccept_ra 1', dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["fc00:123:4567:89ab:cdef::1234/64"], 'accept_ra': True}}}}, out.decode()) def test_static_ipv6_accept_ra_2(self): out, err = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234/64\naccept_ra 2', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'netplan does not support accept_ra=2', err) def test_static_ipv6_accept_ra_unexpected(self): out, err = self.do_test('auto en1\niface en1 inet6 static\n' 'address fc00:0123:4567:89ab:cdef::1234/64\naccept_ra fish', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'unexpected accept_ra value "fish"', err) def test_static_gateway(self): out = self.do_test("""auto en1 iface en1 inet static address 1.2.3.4 netmask 255.0.0.0 gateway 1.1.1.1 iface en1 inet6 static address fc00:0123:4567:89ab:cdef::1234/64 gateway fc00:0123:4567:89ab::1""", dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["1.2.3.4/8", "fc00:123:4567:89ab:cdef::1234/64"], 'gateway4': "1.1.1.1", 'gateway6': "fc00:0123:4567:89ab::1"}}}}, out.decode()) def test_static_dns(self): out = self.do_test("""auto en1 iface en1 inet static address 1.2.3.4 netmask 255.0.0.0 dns-nameservers 1.2.1.1 1.2.2.1 dns-search weird.network iface en1 inet6 static address fc00:0123:4567:89ab:cdef::1234/64 dns-nameservers fc00:0123:4567:89ab:1::1 fc00:0123:4567:89ab:2::1""", dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["1.2.3.4/8", "fc00:123:4567:89ab:cdef::1234/64"], 'nameservers': { 'search': ['weird.network'], 'addresses': ['1.2.1.1', '1.2.2.1', 'fc00:0123:4567:89ab:1::1', 'fc00:0123:4567:89ab:2::1'] }}}}}, out.decode()) def test_static_dns2(self): out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\ndns-search foo foo.bar', dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"], 'nameservers': { 'search': ['foo', 'foo.bar'] }}}}}, out.decode()) def test_static_mtu(self): out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nmtu 1280', dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"], 'mtu': 1280}}}}, out.decode()) def test_static_invalid_mtu(self): out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nmtu fish', expect_success=False) self.assertEqual(b'', out) self.assertIn(b'cannot parse "fish" as an MTU', err) def test_static_two_different_mtus(self): out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nmtu 1280\n' 'iface en1 inet6 static\naddress 2001::1/64\nmtu 9000', expect_success=False) self.assertEqual(b'', out) self.assertIn(b'tried to set MTU=9000, but already have MTU=1280', err) def test_static_hwaddress(self): out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nhwaddress 52:54:00:6b:3c:59', dry_run=True)[0] self.assertEqual(_load_yaml(out), {'network': { 'version': 2, 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"], 'macaddress': '52:54:00:6b:3c:59'}}}}, out.decode()) def test_static_two_different_macs(self): out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nhwaddress 52:54:00:6b:3c:59\n' 'iface en1 inet6 static\naddress 2001::1/64\nhwaddress 52:54:00:6b:3c:58', expect_success=False) self.assertEqual(b'', out) self.assertIn(b'tried to set MAC 52:54:00:6b:3c:58, but already have MAC 52:54:00:6b:3c:59', err) # # configs which are not supported # def test_noauto(self): (out, err) = self.do_test('iface en1 inet dhcp', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'non-automatic interfaces are not supported', err) def test_dhcp_options(self): (out, err) = self.do_test('auto en1\niface en1 inet dhcp\nup myhook', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'option(s) up are not supported for dhcp method', err) def test_mapping(self): (out, err) = self.do_test('mapping en*\n script /some/path/mapscheme\nmap HOME en1-home\n\n' 'auto map1\niface map1 inet dhcp', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'mapping stanza is not supported', err) def test_unknown_allow(self): (out, err) = self.do_test('allow-foo en1\niface en1 inet dhcp', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'Unknown stanza type allow-foo', err) def test_unknown_stanza(self): (out, err) = self.do_test('foo en1\niface en1 inet dhcp', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'Unknown stanza type foo', err) def test_unknown_family(self): (out, err) = self.do_test('auto en1\niface en1 inet7 dhcp', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'Unknown address family inet7', err) def test_unknown_method(self): (out, err) = self.do_test('auto en1\niface en1 inet mangle', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'Unsupported method mangle', err) def test_too_few_fields(self): (out, err) = self.do_test('auto en1\niface en1 inet', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'Expected 3 fields for stanza type iface but got 2', err) def test_too_many_fields(self): (out, err) = self.do_test('auto en1\niface en1 inet dhcp foo', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'Expected 3 fields for stanza type iface but got 4', err) def test_write_file_unsupported(self): (out, err) = self.do_test('iface en1 inet dhcp', expect_success=False) self.assertEqual(out, b'') self.assertIn(b'non-automatic interfaces are not supported', err) # should keep original ifupdown config self.assertTrue(os.path.exists(self.ifaces_path)) class TestInfo(unittest.TestCase): '''Test netplan info''' def test_info_defaults(self): """ Check that 'netplan info' outputs at all, should include website URL """ out = subprocess.check_output(exe_cli + ['info']) self.assertIn(b'features:', out) def test_info_yaml(self): """ Verify that 'netplan info --yaml' output looks a bit like YAML """ out = subprocess.check_output(exe_cli + ['info', '--yaml']) self.assertIn(b'features:', out) def test_info_json(self): """ Verify that 'netplan info --json' output looks a bit like JSON """ out = subprocess.check_output(exe_cli + ['info', '--json']) self.assertIn(b'"features": [', out) class TestIp(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() def test_valid_subcommand(self): p = subprocess.Popen(exe_cli + ['ip'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() self.assertEqual(out, b'') self.assertIn(b'Available command', err) self.assertNotEqual(p.returncode, 0) def test_ip_leases_networkd(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: # match against loopback so as to successfully get a predictable # ifindex f.write('''network: version: 2 renderer: networkd ethernets: enlol: match: name: lo dhcp4: yes ''') fake_netif_lease_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'netif', 'leases') os.makedirs(fake_netif_lease_dir) with open(os.path.join(fake_netif_lease_dir, '1'), 'w') as f: f.write('''THIS IS A FAKE NETIF LEASE FOR LO''') out = subprocess.check_output(exe_cli + ['ip', 'leases', '--root-dir', self.workdir.name, 'lo']) self.assertNotEqual(out, b'') self.assertIn('FAKE NETIF', out.decode('utf-8')) def test_ip_leases_nm(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) mock = MockCmd('nmcli') mock.set_output(nmcli_mock_output) new_path = os.pathsep.join([os.path.dirname(mock.path), os.environ.get('PATH', os.defpath)]) mock_env = dict(os.environ, PATH=new_path) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: f.write('''network: version: 2 renderer: NetworkManager ethernets: {lo: {dhcp4: yes}} ''') fake_lease_dir = os.path.join(self.workdir.name, 'var', 'lib', 'NetworkManager') os.makedirs(fake_lease_dir) # using fake '0000..' UUID from nmcli_mock_output lease_file = 'internal-00000000-0000-0000-0000-000000000000-lo.lease' with open(os.path.join(fake_lease_dir, lease_file), 'w') as f: f.write('''THIS IS A FAKE INTERNAL_NM LEASE FOR LO''') out = subprocess.check_output(exe_cli + ['ip', 'leases', '--root-dir', self.workdir.name, 'lo'], env=mock_env) self.assertIn('FAKE INTERNAL_NM LEASE', out.decode('utf-8')) def test_ip_leases_nm_dhclient_fallback(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) mock = MockCmd('nmcli') mock.set_output(nmcli_mock_output) new_path = os.pathsep.join([os.path.dirname(mock.path), os.environ.get('PATH', os.defpath)]) mock_env = dict(os.environ, PATH=new_path) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: f.write('''network: version: 2 renderer: NetworkManager ethernets: {lo: {dhcp4: yes}} ''') fake_lease_dir = os.path.join(self.workdir.name, 'var', 'lib', 'NetworkManager') os.makedirs(fake_lease_dir) # using fake '0000..' UUID from nmcli_mock_output lease_file = 'dhclient-00000000-0000-0000-0000-000000000000-lo.lease' with open(os.path.join(fake_lease_dir, lease_file), 'w') as f: f.write('''THIS IS A FAKE DHCLIENT_NM LEASE FOR LO''') out = subprocess.check_output(exe_cli + ['ip', 'leases', '--root-dir', self.workdir.name, 'lo'], env=mock_env) self.assertIn('FAKE DHCLIENT_NM LEASE', out.decode('utf-8')) def test_ip_leases_nm_fail(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) mock = MockCmd('nmcli') mock.set_output(nmcli_mock_output) mock.set_returncode(10) new_path = os.pathsep.join([os.path.dirname(mock.path), os.environ.get('PATH', os.defpath)]) mock_env = dict(os.environ, PATH=new_path) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: f.write('''network: version: 2 renderer: NetworkManager ethernets: {lo: {dhcp4: yes}} ''') # the nmcli Mock's return value is 10, indicating an error with self.assertRaises(Exception): subprocess.check_output(exe_cli + ['ip', 'leases', '--root-dir', self.workdir.name, 'lo'], env=mock_env) def test_ip_leases_no_networkd_lease(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: # match against loopback so as to successfully get a predictable # ifindex f.write('''network: version: 2 ethernets: enlol: match: name: lo dhcp4: yes ''') p = subprocess.Popen(exe_cli + ['ip', 'leases', '--root-dir', self.workdir.name, 'enlol'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() self.assertEqual(out, b'') self.assertIn(b'No lease found', err) self.assertNotEqual(p.returncode, 0) def test_ip_leases_no_nm_lease(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) mock = MockCmd('nmcli') mock.set_output('\n') new_path = os.pathsep.join([os.path.dirname(mock.path), os.environ.get('PATH', os.defpath)]) mock_env = dict(os.environ, PATH=new_path) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: f.write('''network: version: 2 renderer: NetworkManager ethernets: enlol: match: name: lo dhcp4: yes ''') # we didn't create a (mock) lease file, therefore expect stderr output p = subprocess.Popen(exe_cli + ['ip', 'leases', '--root-dir', self.workdir.name, 'enlol'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=mock_env) (out, err) = p.communicate() self.assertEqual(out, b'') self.assertIn(b'No lease found', err) self.assertNotEqual(p.returncode, 0) def test_ip_leases_no_netdefs(self): os.environ.setdefault('NETPLAN_GENERATE_PATH', os.path.join(rootdir, 'generate')) c = os.path.join(self.workdir.name, 'etc', 'netplan') os.makedirs(c) with open(os.path.join(c, 'a.yaml'), 'w') as f: f.write('''network: version: 2 renderer: NetworkManager ''') # we didn't create a (mock) lease file, therefore expect stderr output p = subprocess.Popen(exe_cli + ['ip', 'leases', '--root-dir', self.workdir.name, 'enlol'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() self.assertEqual(out, b'') self.assertIn(b"No lease found for interface 'enlol' (not managed by Netplan)", err) self.assertNotEqual(p.returncode, 0) unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/ctests/000077500000000000000000000000001457004145200154645ustar00rootroot00000000000000netplan-1.0/tests/ctests/fixtures/000077500000000000000000000000001457004145200173355ustar00rootroot00000000000000netplan-1.0/tests/ctests/fixtures/bond.yaml000066400000000000000000000002331457004145200211410ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: eth0: dhcp4: false bonds: bond0: dhcp4: yes interfaces: - eth0 netplan-1.0/tests/ctests/fixtures/bridge.yaml000066400000000000000000000002341457004145200214540ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: enp3s0: dhcp4: no bridges: br0: dhcp4: yes interfaces: - enp3s0 netplan-1.0/tests/ctests/fixtures/invalid_route.yaml000066400000000000000000000002371457004145200230670ustar00rootroot00000000000000network: bridges: br0: interfaces: - ens3 routes: - to: 0/0 via: 100.64.0.1 ethernets: ens3: {} version: 2 netplan-1.0/tests/ctests/fixtures/missing_interface.yaml000066400000000000000000000002111457004145200237040ustar00rootroot00000000000000network: bridges: br0: interfaces: - ens3 routes: - to: default via: 100.64.0.1 version: 2 netplan-1.0/tests/ctests/fixtures/nullable.yaml000066400000000000000000000000621457004145200220150ustar00rootroot00000000000000network: ethernets: eth0: dhcp4: NULL netplan-1.0/tests/ctests/fixtures/optional.yaml000066400000000000000000000001471457004145200220500ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: eth0: dhcp4: no optional: true netplan-1.0/tests/ctests/fixtures/ovs.yaml000066400000000000000000000021271457004145200210320ustar00rootroot00000000000000network: version: 2 openvswitch: protocols: [OpenFlow13, OpenFlow14, OpenFlow15] ports: - [patch0-1, patch1-0] ssl: ca-cert: /some/ca-cert.pem certificate: /another/cert.pem private-key: /private/key.pem external-ids: somekey: somevalue other-config: key: value ethernets: eth0: addresses: [10.5.32.26/20] openvswitch: external-ids: iface-id: mylocaliface other-config: disable-in-band: false eth1: {} bonds: bond0: interfaces: [patch1-0, eth1] openvswitch: lacp: passive parameters: mode: balance-tcp bridges: ovs0: addresses: [10.5.48.11/20] interfaces: [patch0-1, eth0, bond0] openvswitch: protocols: [OpenFlow10, OpenFlow11, OpenFlow12] controller: addresses: [unix:/var/run/openvswitch/ovs0.mgmt] connection-mode: out-of-band fail-mode: secure mcast-snooping: true external-ids: iface-id: myhostname other-config: disable-in-band: true netplan-1.0/tests/ctests/fixtures/ovs2.yaml000066400000000000000000000002671457004145200211170ustar00rootroot00000000000000network: version: 2 openvswitch: ports: - [patch0-1, patch1-0] bonds: bond0: interfaces: [patch1-0] bridges: ovs0: interfaces: [patch0-1, bond0] netplan-1.0/tests/ctests/fixtures/sriov.yaml000066400000000000000000000003551457004145200213660ustar00rootroot00000000000000network: version: 2 renderer: networkd ethernets: eno1: embedded-switch-mode: "switchdev" virtual-function-count: 2 enp1s16f1: link: eno1 vf1: match: name: enp1s16f[2-3] link: eno1 netplan-1.0/tests/ctests/meson.build000066400000000000000000000014161457004145200176300ustar00rootroot00000000000000tests = { 'test_netplan_parser': false, 'test_netplan_state': false, 'test_netplan_error': false, 'test_netplan_misc': false, 'test_netplan_validation': false, 'test_netplan_keyfile': false, 'test_netplan_nm': false, 'test_netplan_openvswitch': false, } cmocka = dependency('cmocka', required: true) foreach name, should_fail: tests exe = executable(name, '@0@.c'.format(name), include_directories: [inc, inc_internal], dependencies: [cmocka, glib, gio, yaml, uuid], link_with: libnetplan_testing, c_args: [ '-DFIXTURESDIR="' + meson.project_source_root() + '/tests/ctests/fixtures"', '-Wno-deprecated-declarations', '-D_GNU_SOURCE', ], install: false, ) test(name, exe, should_fail: should_fail) endforeach netplan-1.0/tests/ctests/test_netplan_error.c000066400000000000000000000024001457004145200215350ustar00rootroot00000000000000#include #include #include #include #include #include "netplan.h" #include "parse.h" #include "util.h" #include "util-internal.h" void test_netplan_error_message(__unused void** state) { const gchar* message = "it failed"; char error_message[100] = {0}; GError *gerror = g_error_new(1, 2, "%s: error message", message); netplan_error_message(gerror, error_message, sizeof(error_message) - 1); assert_string_equal(error_message, "it failed: error message"); netplan_error_clear(&gerror); } void test_netplan_error_code(__unused void** state) { GError *gerror = g_error_new(1234, 5678, "%s: error message", "it failed"); uint64_t error_code = netplan_error_code(gerror); GQuark domain = error_code >> 32; gint error = (gint) error_code; assert_int_equal(domain, 1234); assert_int_equal(error, 5678); netplan_error_clear(&gerror); } int setup(__unused void** state) { return 0; } int tear_down(__unused void** state) { return 0; } int main() { const struct CMUnitTest tests[] = { cmocka_unit_test(test_netplan_error_message), cmocka_unit_test(test_netplan_error_code), }; return cmocka_run_group_tests(tests, setup, tear_down); } netplan-1.0/tests/ctests/test_netplan_keyfile.c000066400000000000000000000200731457004145200220420ustar00rootroot00000000000000#include #include #include #include #include #include "netplan.h" #include "parse-nm.h" #include "parse.h" #include "util-internal.h" #include "test_utils.h" #include "test_utils_keyfile.h" void test_load_keyfile_wifi_wpa_eap(__unused void** state) { NetplanState *np_state = NULL; NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; NetplanWifiAccessPoint* ap = NULL; const char* keyfile = "[connection]\n" "id=mywifi\n" "uuid=03c8f2a7-268d-4765-b626-efcc02dd686c\n" "type=wifi\n" "interface-name=wlp2s0\n" "[wifi]\n" "mode=infrastructure\n" "ssid=mywifi\n" "[wifi-security]\n" "auth-alg=open\n" "key-mgmt=wpa-eap\n" "[802-1x]\n" "ca-cert=/path/to/cert.crt\n" "eap=peap;\n" "identity=username\n" "password=mypassword\n" "phase2-auth=mschapv2\n" "[ipv4]\n" "method=auto\n"; np_state = load_keyfile_string_to_netplan_state(keyfile); netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); ap = g_hash_table_lookup(netdef->access_points, "mywifi"); assert_string_equal(netdef->id, "NM-03c8f2a7-268d-4765-b626-efcc02dd686c"); assert_string_equal(ap->ssid, "mywifi"); assert_string_equal(ap->auth.identity, "username"); assert_string_equal(ap->auth.ca_certificate, "/path/to/cert.crt"); netplan_state_clear(&np_state); } void test_load_keyfile_simple_wireguard(__unused void** state) { NetplanState *np_state = NULL; NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; const char* keyfile = "[connection]\n" "id=wg0\n" "type=wireguard\n" "uuid=19f501f5-9984-429a-a8b5-3f5a89aa460c\n" "interface-name=wg0\n" "[ipv4]\n" "method=auto\n"; np_state = load_keyfile_string_to_netplan_state(keyfile); netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_string_equal(netdef->id, "wg0"); assert_null(netdef->wireguard_peers); netplan_state_clear(&np_state); } void test_load_keyfile_wireguard_with_key_and_peer(__unused void** state) { NetplanState *np_state = NULL; NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; const char* keyfile = "[connection]\n" "id=client-wg0\n" "type=wireguard\n" "uuid=6352c897-174c-4f61-9623-556eddad05b2\n" "interface-name=wg0\n" "[wireguard]\n" "private-key=aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A=\n" "[wireguard-peer.cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI=]\n" "endpoint=1.2.3.4:12345\n" "allowed-ips=192.168.0.0/24;\n" "[ipv4]\n" "method=auto\n"; np_state = load_keyfile_string_to_netplan_state(keyfile); netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_string_equal(netdef->id, "wg0"); assert_string_equal(netdef->tunnel.private_key, "aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A="); NetplanWireguardPeer* peer = g_array_index(netdef->wireguard_peers, NetplanWireguardPeer*, 0); assert_string_equal(peer->public_key, "cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI="); assert_string_equal(peer->endpoint, "1.2.3.4:12345"); gchar* allowed_ip = g_array_index(peer->allowed_ips, gchar*, 0); assert_string_equal(allowed_ip, "192.168.0.0/24"); netplan_state_clear(&np_state); } void test_load_keyfile_wireguard_with_bad_peer_key(__unused void** state) { NetplanState *np_state = NULL; NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; const char* keyfile = "[connection]\n" "id=client-wg0\n" "type=wireguard\n" "uuid=6352c897-174c-4f61-9623-556eddad05b2\n" "interface-name=wg0\n" "[wireguard]\n" "private-key=aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A=\n" "[wireguard-peer.this_is_not_a_valid_peer_public_key]\n" "endpoint=1.2.3.4:12345\n" "allowed-ips=192.168.0.0/24;\n" "[ipv4]\n" "method=auto\n"; np_state = load_keyfile_string_to_netplan_state(keyfile); netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_null(netdef->wireguard_peers); netplan_state_clear(&np_state); } void test_load_keyfile_vxlan(__unused void** state) { NetplanState *np_state = NULL; NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; const char* keyfile = "[connection]\n" "id=vxlan0\n" "type=vxlan\n" "uuid=6352c897-174c-4f61-9623-556eddad05b2\n" "interface-name=vxlan0\n" "[vxlan]\n" "id=10\n" "local=1.2.3.4\n" "remote=4.3.2.1\n" "[ipv4]\n" "method=auto\n"; np_state = load_keyfile_string_to_netplan_state(keyfile); netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_string_equal(netdef->id, "vxlan0"); assert_int_equal(netdef->vxlan->vni, 10); assert_string_equal(netdef->tunnel.local_ip, "1.2.3.4"); assert_string_equal(netdef->tunnel.remote_ip, "4.3.2.1"); netplan_state_clear(&np_state); } void test_load_keyfile_multiple_addresses_and_routes(__unused void** state) { NetplanState *np_state = NULL; NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; const char* keyfile = "[connection]\n" "id=netplan-enp3s0\n" "type=ethernet\n" "interface-name=enp3s0\n" "uuid=6352c897-174c-4f61-9623-556eddad05b2\n" "[ipv4]\n" "method=manual\n" "address1=10.100.1.38/24\n" "address2=10.100.1.39/24\n" "route1=0.0.0.0/0,10.100.1.1\n" "route1_options=onlink=true,initrwnd=33,initcwnd=44,mtu=1024,table=102\n" "route2=192.168.0.0/24,1.2.3.4\n" "route2_options=onlink=true,initrwnd=33,initcwnd=44,mtu=1024,table=103\n" "[ipv6]\n" "method=manual\n" "address1=2001:cafe:face::1/64\n" "address2=2001:cafe:face::2/64\n" "ip6-privacy=0\n" "route1=::/0,2001:cafe:face::3/64\n"; np_state = load_keyfile_string_to_netplan_state(keyfile); netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_string_equal(netdef->id, "NM-6352c897-174c-4f61-9623-556eddad05b2"); netplan_state_clear(&np_state); } void test_load_keyfile_route_options_without_route(__unused void** state) { NetplanState *np_state = NULL; NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; /* Should this even be allowed? */ const char* keyfile = "[connection]\n" "id=netplan-enp3s0\n" "type=ethernet\n" "interface-name=enp3s0\n" "uuid=6352c897-174c-4f61-9623-556eddad05b2\n" "[ipv4]\n" "method=manual\n" "address1=10.100.1.38/24\n" "address2=10.100.1.39/24\n" "route1_options=onlink=true,initrwnd=33,initcwnd=44,mtu=1024,table=102,src=10.10.10.11\n"; np_state = load_keyfile_string_to_netplan_state(keyfile); netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_string_equal(netdef->id, "NM-6352c897-174c-4f61-9623-556eddad05b2"); netplan_state_clear(&np_state); } int setup(__unused void** state) { return 0; } int tear_down(__unused void** state) { return 0; } int main() { const struct CMUnitTest tests[] = { cmocka_unit_test(test_load_keyfile_wifi_wpa_eap), cmocka_unit_test(test_load_keyfile_simple_wireguard), cmocka_unit_test(test_load_keyfile_wireguard_with_key_and_peer), cmocka_unit_test(test_load_keyfile_wireguard_with_bad_peer_key), cmocka_unit_test(test_load_keyfile_vxlan), cmocka_unit_test(test_load_keyfile_multiple_addresses_and_routes), cmocka_unit_test(test_load_keyfile_route_options_without_route), }; return cmocka_run_group_tests(tests, setup, tear_down); } netplan-1.0/tests/ctests/test_netplan_misc.c000066400000000000000000000374001457004145200213470ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "netplan.h" #include "parse.h" #include "util-internal.h" #include "types-internal.h" #include "types.h" #include "test_utils.h" void test_netplan_get_optional(__unused void** state) { NetplanState* np_state = load_fixture_to_netplan_state("optional.yaml"); NetplanNetDefinition* interface = netplan_state_get_netdef(np_state, "eth0"); gboolean optional = _netplan_netdef_get_optional(interface); assert_true(optional); netplan_state_clear(&np_state); } void test_netplan_get_id_from_nm_filepath_no_ssid(__unused void **state) { const char* filename = "/some/rootdir/run/NetworkManager/system-connections/netplan-some-id.nmconnection"; char id[16]; ssize_t bytes_copied = netplan_get_id_from_nm_filepath(filename, NULL, id, sizeof(id)); assert_string_equal(id, "some-id"); assert_int_equal(bytes_copied, 8); // size of some-id + null byte } void test_netplan_get_id_from_nm_filepath_no_nmconnection(__unused void **state) { const char* filename = "/some/rootdir/run/NetworkManager/system-connections/netplan-some-id"; char id[16]; ssize_t bytes_copied = netplan_get_id_from_nm_filepath(filename, NULL, id, sizeof(id)); assert_int_equal(bytes_copied, 0); } void test_netplan_get_id_from_nm_filepath_with_ssid(__unused void **state) { const char* filename = "/run/NetworkManager/system-connections/netplan-some-id-SOME-SSID.nmconnection"; char id[16]; ssize_t bytes_copied = netplan_get_id_from_nm_filepath(filename, "SOME-SSID", id, sizeof(id)); assert_string_equal(id, "some-id"); assert_int_equal(bytes_copied, 8); // size of some-id + null byte } void test_netplan_get_id_from_nm_filepath_buffer_is_too_small(__unused void **state) { const char* filename = "/run/NetworkManager/system-connections/netplan-some-id-SOME-SSID.nmconnection"; char id[7]; ssize_t bytes_copied = netplan_get_id_from_nm_filepath(filename, "SOME-SSID", id, sizeof(id)); assert_int_equal(bytes_copied, NETPLAN_BUFFER_TOO_SMALL); } void test_netplan_get_id_from_nm_filepath_buffer_is_the_exact_size(__unused void **state) { const char* filename = "/run/NetworkManager/system-connections/netplan-some-id-SOME-SSID.nmconnection"; char id[8]; ssize_t bytes_copied = netplan_get_id_from_nm_filepath(filename, "SOME-SSID", id, sizeof(id)); assert_string_equal(id, "some-id"); assert_int_equal(bytes_copied, 8); // size of some-id + null byte } void test_netplan_get_id_from_nm_filepath_filename_is_malformed(__unused void **state) { const char* filename = "INVALID/netplan-some-id.nmconnection"; char id[8]; ssize_t bytes_copied = netplan_get_id_from_nm_filepath(filename, "SOME-SSID", id, sizeof(id)); assert_int_equal(bytes_copied, 0); } void test_netplan_netdef_get_output_filename_nm_with_ssid(__unused void** state) { NetplanNetDefinition netdef; const char* expected = "/run/NetworkManager/system-connections/netplan-enlol3s0-home-network.nmconnection"; size_t expected_size = strlen(expected) + 1; char out_buffer[100] = { 0 }; netdef.backend = NETPLAN_BACKEND_NM; netdef.id = "enlol3s0"; const char* ssid = "home-network"; ssize_t ret = netplan_netdef_get_output_filename(&netdef, ssid, out_buffer, sizeof(out_buffer) - 1); assert_int_equal(ret, expected_size); assert_string_equal(out_buffer, expected); } void test_netplan_netdef_get_output_filename_nm_without_ssid(__unused void** state) { NetplanNetDefinition netdef; const char* expected = "/run/NetworkManager/system-connections/netplan-enlol3s0.nmconnection"; size_t expected_size = strlen(expected) + 1; char out_buffer[100] = { 0 }; netdef.backend = NETPLAN_BACKEND_NM; netdef.id = "enlol3s0"; ssize_t ret = netplan_netdef_get_output_filename(&netdef, NULL, out_buffer, sizeof(out_buffer) - 1); assert_int_equal(ret, expected_size); assert_string_equal(out_buffer, expected); } void test_netplan_netdef_get_output_filename_networkd(__unused void** state) { NetplanNetDefinition netdef; const char* expected = "/run/systemd/network/10-netplan-enlol3s0.network"; size_t expected_size = strlen(expected) + 1; char out_buffer[100] = { 0 }; netdef.backend = NETPLAN_BACKEND_NETWORKD; netdef.id = "enlol3s0"; ssize_t ret = netplan_netdef_get_output_filename(&netdef, NULL, out_buffer, sizeof(out_buffer) - 1); assert_int_equal(ret, expected_size); assert_string_equal(out_buffer, expected); } void test_netplan_netdef_get_output_filename_buffer_is_too_small(__unused void** state) { NetplanNetDefinition netdef; char out_buffer[16] = { 0 }; netdef.backend = NETPLAN_BACKEND_NETWORKD; netdef.id = "enlol3s0"; ssize_t ret = netplan_netdef_get_output_filename(&netdef, NULL, out_buffer, sizeof(out_buffer) - 1); assert_int_equal(ret, NETPLAN_BUFFER_TOO_SMALL); } void test_netplan_netdef_get_output_filename_invalid_backend(__unused void** state) { NetplanNetDefinition netdef; char out_buffer[16] = { 0 }; netdef.backend = NETPLAN_BACKEND_NONE; netdef.id = "enlol3s0"; ssize_t ret = netplan_netdef_get_output_filename(&netdef, NULL, out_buffer, sizeof(out_buffer) - 1); assert_int_equal(ret, 0); } void test_netplan_netdef_write_yaml(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " ethernets:\n" " eth0:\n" " dhcp4: true"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanNetDefinition* interface = netplan_state_get_netdef(np_state, "eth0"); char template[] = "/tmp/netplan.XXXXXX"; // no need to free() rootdir, as it will modify the template[] buffer char *rootdir = mkdtemp(template); char etc[24] = {0}; char etc_netplan[32] = {0}; snprintf(etc, 24, "%s/etc", rootdir); snprintf(etc_netplan, 32, "%s/netplan", etc); mkdir(etc, 0770); mkdir(etc_netplan, 0770); /* Check API call */ NetplanError* err = NULL; assert_true(netplan_netdef_write_yaml(np_state, interface, rootdir, &err)); assert_true(err == NULL); /* Check file exists */ struct stat st = {0}; char output_yaml[53] = {0}; snprintf(output_yaml, 53, "%s/10-netplan-eth0.yaml", etc_netplan); assert_true(stat(output_yaml, &st) == 0); /* Check file contents */ FILE *fd = fopen(output_yaml, "r"); char file_buffer[600] = {0}; assert_true(fread(file_buffer, 1, strlen(yaml), fd) > 0); assert_string_equal(yaml, file_buffer); /* Cleanup */ netplan_state_clear(&np_state); fclose(fd); remove(output_yaml); rmdir(etc_netplan); rmdir(etc); rmdir(rootdir); } void test_netplan_netdef_write_yaml_90NM(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" //" renderer: NetworkManager\n" //FIXME: renderer get's eaten by the API call... " ethernets:\n" " eth0:\n" " dhcp4: true\n" " networkmanager:\n" " uuid: \"990548be-01ed-42d7-9f9f-cd4966b25c08\""; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanNetDefinition* interface = netplan_state_get_netdef(np_state, "eth0"); char template[] = "/tmp/netplan.XXXXXX"; // no need to free() rootdir, as it will modify the template[] buffer char *rootdir = mkdtemp(template); char etc[24] = {0}; char etc_netplan[32] = {0}; snprintf(etc, 24, "%s/etc", rootdir); snprintf(etc_netplan, 32, "%s/netplan", etc); mkdir(etc, 0770); mkdir(etc_netplan, 0770); /* Check API call */ NetplanError* err = NULL; assert_true(netplan_netdef_write_yaml(np_state, interface, rootdir, &err)); assert_true(err == NULL); /* Check file exists */ struct stat st = {0}; char output_yaml[80] = {0}; snprintf(output_yaml, 80, "%s/90-NM-990548be-01ed-42d7-9f9f-cd4966b25c08.yaml", etc_netplan); assert_true(stat(output_yaml, &st) == 0); /* Check file contents */ FILE *fd = fopen(output_yaml, "r"); char file_buffer[600] = {0}; assert_true(fread(file_buffer, 1, strlen(yaml), fd) > 0); assert_string_equal(yaml, file_buffer); /* Cleanup */ netplan_state_clear(&np_state); fclose(fd); remove(output_yaml); rmdir(etc_netplan); rmdir(etc); rmdir(rootdir); } void test_util_is_route_present(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " ethernets:\n" " eth0:\n" " routing-policy:\n" " - from: 10.0.0.1\n" " table: 1001\n" " - from: 10.0.0.2\n" " table: 1002\n" " routes:\n" " - to: 0.0.0.0/0\n" " via: 10.0.0.200\n" " table: 1002\n" " - to: 0.0.0.0/0\n" " via: 10.0.0.200\n" " table: 1001\n" " - to: 192.168.0.0/24\n" " via: 10.20.30.40\n" " - to: 192.168.0.0/24\n" " scope: link\n" " - to: default\n" " via: abcd::1\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); NetplanIPRoute* route = g_new0(NetplanIPRoute, 1); route->family = AF_INET; route->metric = NETPLAN_METRIC_UNSPEC; route->table = 1001; route->to = "0.0.0.0/0"; route->via = "10.0.0.200"; route->from = NULL; assert_true(is_route_present(netdef, route)); route->table = 1002; route->to = "0.0.0.0/0"; route->via = "10.0.0.200"; route->from = NULL; assert_true(is_route_present(netdef, route)); route->table = NETPLAN_ROUTE_TABLE_UNSPEC; route->to = "192.168.0.0/24"; route->via = "10.20.30.40"; route->from = NULL; assert_true(is_route_present(netdef, route)); route->table = 1002; route->to = "0.0.0.0/0"; route->via = "10.0.0.100"; route->from = NULL; assert_false(is_route_present(netdef, route)); route->table = 1003; route->to = "0.0.0.0/0"; route->via = "10.0.0.200"; route->from = NULL; assert_false(is_route_present(netdef, route)); route->table = 1001; route->to = "default"; route->via = "10.0.0.200"; route->from = NULL; assert_true(is_route_present(netdef, route)); route->table = NETPLAN_ROUTE_TABLE_UNSPEC; route->family = AF_INET6; route->to = "::/0"; route->via = "abcd::1"; route->from = NULL; assert_true(is_route_present(netdef, route)); route->table = NETPLAN_ROUTE_TABLE_UNSPEC; route->family = AF_INET; route->to = "192.168.0.0/24"; route->via = NULL; route->from = NULL; route->scope = "link"; assert_true(is_route_present(netdef, route)); g_free(route); netplan_state_clear(&np_state); } void test_util_is_route_rule_present(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " ethernets:\n" " eth0:\n" " routing-policy:\n" " - from: 10.0.0.1\n" " table: 1001\n" " - from: 10.0.0.2\n" " table: 1002\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); NetplanIPRule* rule = g_new0(NetplanIPRule, 1); reset_ip_rule(rule); rule->family = AF_INET; rule->table = 1001; rule->from = "10.0.0.1"; assert_true(is_route_rule_present(netdef, rule)); rule->table = 1003; rule->from = "10.0.0.1"; assert_false(is_route_rule_present(netdef, rule)); g_free(rule); netplan_state_clear(&np_state); } void test_util_is_string_in_array(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " ethernets:\n" " eth0:\n" " nameservers:\n" " addresses: [8.8.8.8, 8.8.4.4]\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_true(is_string_in_array(netdef->ip4_nameservers, "8.8.8.8")); assert_false(is_string_in_array(netdef->ip4_nameservers, "somethingelse")); netplan_state_clear(&np_state); } void test_util_get_link_local_true(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " ethernets:\n" " eth0:\n" " link-local: [ipv4, ipv6]\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_true(netplan_netdef_get_link_local_ipv4(netdef)); assert_true(netplan_netdef_get_link_local_ipv6(netdef)); netplan_state_clear(&np_state); } void test_util_get_link_local_false(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " ethernets:\n" " eth0:\n" " link-local: []\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_false(netplan_netdef_get_link_local_ipv4(netdef)); assert_false(netplan_netdef_get_link_local_ipv6(netdef)); netplan_state_clear(&np_state); } void test_normalize_ip_address(__unused void** state) { assert_string_equal(normalize_ip_address("default", AF_INET), "0.0.0.0/0"); assert_string_equal(normalize_ip_address("default", AF_INET6), "::/0"); assert_string_equal(normalize_ip_address("0.0.0.0/0", AF_INET), "0.0.0.0/0"); } int setup(__unused void** state) { return 0; } int tear_down(__unused void** state) { return 0; } int main() { const struct CMUnitTest tests[] = { cmocka_unit_test(test_netplan_get_optional), cmocka_unit_test(test_netplan_get_id_from_nm_filepath_no_ssid), cmocka_unit_test(test_netplan_get_id_from_nm_filepath_no_nmconnection), cmocka_unit_test(test_netplan_get_id_from_nm_filepath_with_ssid), cmocka_unit_test(test_netplan_get_id_from_nm_filepath_buffer_is_too_small), cmocka_unit_test(test_netplan_get_id_from_nm_filepath_buffer_is_the_exact_size), cmocka_unit_test(test_netplan_get_id_from_nm_filepath_filename_is_malformed), cmocka_unit_test(test_netplan_netdef_get_output_filename_nm_with_ssid), cmocka_unit_test(test_netplan_netdef_get_output_filename_nm_without_ssid), cmocka_unit_test(test_netplan_netdef_get_output_filename_networkd), cmocka_unit_test(test_netplan_netdef_get_output_filename_buffer_is_too_small), cmocka_unit_test(test_netplan_netdef_get_output_filename_invalid_backend), cmocka_unit_test(test_netplan_netdef_write_yaml), cmocka_unit_test(test_netplan_netdef_write_yaml_90NM), cmocka_unit_test(test_util_is_route_present), cmocka_unit_test(test_util_is_route_rule_present), cmocka_unit_test(test_util_is_string_in_array), cmocka_unit_test(test_normalize_ip_address), cmocka_unit_test(test_util_get_link_local_true), cmocka_unit_test(test_util_get_link_local_false), }; return cmocka_run_group_tests(tests, setup, tear_down); } netplan-1.0/tests/ctests/test_netplan_nm.c000066400000000000000000000016031457004145200210220ustar00rootroot00000000000000#include #include #include #include #include #include "netplan.h" #include "util-internal.h" #include "test_utils.h" /* Trying to write an empty netplan_state should return True without * actually writing anything. */ void test_write_empty_state(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " renderer: NetworkManager\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); assert_true(netplan_state_finish_nm_write(np_state, NULL, NULL)); netplan_state_clear(&np_state); } int setup(__unused void** state) { return 0; } int tear_down(__unused void** state) { return 0; } int main() { const struct CMUnitTest tests[] = { cmocka_unit_test(test_write_empty_state), }; return cmocka_run_group_tests(tests, setup, tear_down); } netplan-1.0/tests/ctests/test_netplan_openvswitch.c000066400000000000000000000023261457004145200227640ustar00rootroot00000000000000#include #include #include #include #include #include "netplan.h" #include "util-internal.h" #include "validation.h" #include "test_utils.h" void test_write_ovs_bond_interfaces_null_bridge(__unused void** state) { NetplanNetDefinition* netdef = g_malloc0(sizeof(NetplanNetDefinition)); netdef->bridge = NULL; assert_null(write_ovs_bond_interfaces(NULL, netdef, NULL, NULL)); g_free(netdef); } void test_validate_ovs_target(__unused void** state) { assert_true(validate_ovs_target(TRUE, "10.2.3.4:12345")); assert_true(validate_ovs_target(TRUE, "10.2.3.4")); assert_true(validate_ovs_target(TRUE, "[::1]:12345")); assert_true(validate_ovs_target(TRUE, "[::1]")); assert_true(validate_ovs_target(FALSE, "12345:10.2.3.4")); assert_true(validate_ovs_target(FALSE, "12345:[::1]")); } int setup(__unused void** state) { return 0; } int tear_down(__unused void** state) { return 0; } int main() { const struct CMUnitTest tests[] = { cmocka_unit_test(test_write_ovs_bond_interfaces_null_bridge), cmocka_unit_test(test_validate_ovs_target), }; return cmocka_run_group_tests(tests, setup, tear_down); } netplan-1.0/tests/ctests/test_netplan_parser.c000066400000000000000000000203471457004145200217120ustar00rootroot00000000000000#include #include #include #include #include #include #include "netplan.h" #include "parse.h" #include "util.h" #include "util-internal.h" #include "test_utils.h" void test_netplan_parser_new_parser(__unused void** state) { NetplanParser* npp = netplan_parser_new(); assert_non_null(npp); netplan_parser_clear(&npp); } void test_netplan_parser_load_yaml(__unused void** state) { const char* filename = FIXTURESDIR "/bridge.yaml"; GError *error = NULL; NetplanParser* npp = netplan_parser_new(); gboolean res = netplan_parser_load_yaml(npp, filename, &error); assert_true(res); netplan_parser_clear(&npp); } void test_netplan_parser_load_yaml_from_fd(__unused void** state) { const char* filename = FIXTURESDIR "/bridge.yaml"; FILE* f = fopen(filename, "r"); GError *error = NULL; NetplanParser* npp = netplan_parser_new(); gboolean res = netplan_parser_load_yaml_from_fd(npp, fileno(f), &error); assert_true(res); netplan_parser_clear(&npp); netplan_error_clear(&error); fclose(f); } void test_netplan_parser_load_nullable_fields(__unused void** state) { const char* filename = FIXTURESDIR "/nullable.yaml"; FILE* f = fopen(filename, "r"); GError *error = NULL; NetplanParser* npp = netplan_parser_new(); assert_null(npp->null_fields); gboolean res = netplan_parser_load_nullable_fields(npp, fileno(f), &error); assert_true(res); assert_non_null(npp->null_fields); assert_true(g_hash_table_contains(npp->null_fields, "\tnetwork\tethernets\teth0\tdhcp4")); netplan_parser_clear(&npp); netplan_error_clear(&error); fclose(f); } void test_netplan_parser_load_nullable_overrides(__unused void** state) { const char* filename = FIXTURESDIR "/optional.yaml"; FILE* f = fopen(filename, "r"); GError *error = NULL; NetplanParser* npp = netplan_parser_new(); assert_null(npp->null_overrides); gboolean res = netplan_parser_load_nullable_overrides(npp, fileno(f), "hint.yaml", &error); assert_true(res); assert_non_null(npp->null_overrides); assert_string_equal(g_hash_table_lookup(npp->null_overrides, "\tnetwork\trenderer"), "hint.yaml"); assert_string_equal(g_hash_table_lookup(npp->null_overrides, "\tnetwork\tethernets\teth0"), "hint.yaml"); netplan_parser_clear(&npp); netplan_error_clear(&error); fclose(f); } void test_netplan_parser_interface_has_bridge_netdef(__unused void** state) { NetplanState *np_state = load_fixture_to_netplan_state("bridge.yaml"); NetplanNetDefinition* interface = netplan_state_get_netdef(np_state, "enp3s0"); NetplanNetDefinition* bridge = netplan_netdef_get_bridge_link(interface); assert_non_null(interface); assert_non_null(bridge); assert_ptr_equal(interface->bridge_link, bridge); netplan_state_clear(&np_state); } void test_netplan_parser_interface_has_bond_netdef(__unused void** state) { NetplanState* np_state = load_fixture_to_netplan_state("bond.yaml"); NetplanNetDefinition* interface = netplan_state_get_netdef(np_state, "eth0"); NetplanNetDefinition* bond = netplan_netdef_get_bond_link(interface); assert_non_null(interface); assert_non_null(bond); assert_ptr_equal(interface->bond_link, bond); netplan_state_clear(&np_state); } void test_netplan_parser_interface_has_peer_netdef(__unused void** state) { NetplanState* np_state = load_fixture_to_netplan_state("ovs.yaml"); NetplanNetDefinition* patch0 = netplan_state_get_netdef(np_state, "patch0-1"); NetplanNetDefinition* patch1 = netplan_netdef_get_peer_link(patch0); patch0 = netplan_netdef_get_peer_link(patch1); assert_non_null(patch0); assert_non_null(patch1); assert_ptr_equal(patch0->peer_link, patch1); assert_ptr_equal(patch1->peer_link, patch0); netplan_state_clear(&np_state); } void test_netplan_parser_sriov_embedded_switch(__unused void** state) { char embedded_switch[16]; NetplanState* np_state = load_fixture_to_netplan_state("sriov.yaml"); NetplanNetDefinition* interface = netplan_state_get_netdef(np_state, "eno1"); _netplan_netdef_get_embedded_switch_mode(interface, embedded_switch, sizeof(embedded_switch) - 1); assert_string_equal(embedded_switch, "switchdev"); netplan_state_clear(&np_state); } /* process_document() shouldn't return a missing interface as error if a previous error happened * LP#2000324 */ void test_netplan_parser_process_document_proper_error(__unused void** state) { NetplanParser *npp = netplan_parser_new(); yaml_document_t *doc = &npp->doc; GError *error = NULL; const char* filepath = FIXTURESDIR "/invalid_route.yaml"; load_yaml(filepath, doc, NULL); char* source = g_strdup(filepath); npp->sources = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); g_hash_table_add(npp->sources, source); npp->ids_in_file = g_hash_table_new(g_str_hash, NULL); npp->current.filepath = g_strdup(filepath); process_document(npp, &error); yaml_document_delete(doc); g_free((void *)npp->current.filepath); npp->current.filepath = NULL; g_hash_table_destroy(npp->ids_in_file); npp->ids_in_file = NULL; netplan_parser_clear(&npp); /* In this instance the interface IS defined and the actual problem is the malformed IP address */ gboolean found = strstr(error->message, "invalid IP family '-1'") != NULL; netplan_error_clear(&error); assert_true(found); } void test_netplan_parser_process_document_missing_interface_error(__unused void** state) { NetplanParser *npp = netplan_parser_new(); yaml_document_t *doc = &npp->doc; GError *error = NULL; const char* filepath = FIXTURESDIR "/missing_interface.yaml"; load_yaml(filepath, doc, NULL); char* source = g_strdup(filepath); npp->sources = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); g_hash_table_add(npp->sources, source); npp->ids_in_file = g_hash_table_new(g_str_hash, NULL); npp->current.filepath = g_strdup(filepath); process_document(npp, &error); yaml_document_delete(doc); g_free((void *)npp->current.filepath); npp->current.filepath = NULL; g_hash_table_destroy(npp->ids_in_file); npp->ids_in_file = NULL; netplan_parser_clear(&npp); gboolean found = strstr(error->message, "br0: interface 'ens3' is not defined") != NULL; netplan_error_clear(&error); assert_true(found); } void test_nm_device_backend_is_nm_by_default(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " nm-devices:\n" " device0:\n" " networkmanager:\n" " uuid: db5f0f67-1f4c-4d59-8ab8-3d278389cf87\n" " name: connection-123\n" " passthrough:\n" " connection.type: vpn\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_true(netdef->backend == NETPLAN_BACKEND_NM); netplan_state_clear(&np_state); } int setup(__unused void** state) { return 0; } int tear_down(__unused void** state) { return 0; } int main() { const struct CMUnitTest tests[] = { cmocka_unit_test(test_netplan_parser_new_parser), cmocka_unit_test(test_netplan_parser_load_yaml), cmocka_unit_test(test_netplan_parser_load_yaml_from_fd), cmocka_unit_test(test_netplan_parser_load_nullable_fields), cmocka_unit_test(test_netplan_parser_load_nullable_overrides), cmocka_unit_test(test_netplan_parser_interface_has_bridge_netdef), cmocka_unit_test(test_netplan_parser_interface_has_bond_netdef), cmocka_unit_test(test_netplan_parser_interface_has_peer_netdef), cmocka_unit_test(test_netplan_parser_sriov_embedded_switch), cmocka_unit_test(test_netplan_parser_process_document_proper_error), cmocka_unit_test(test_netplan_parser_process_document_missing_interface_error), cmocka_unit_test(test_nm_device_backend_is_nm_by_default), }; return cmocka_run_group_tests(tests, setup, tear_down); } netplan-1.0/tests/ctests/test_netplan_state.c000066400000000000000000000042451457004145200215350ustar00rootroot00000000000000#include #include #include #include #include #include "netplan.h" #include "parse.h" #include "util-internal.h" #include "test_utils.h" void test_netplan_state_new_state(__unused void** state) { NetplanState* np_state = netplan_state_new(); assert_non_null(np_state); netplan_state_clear(&np_state); } void test_netplan_state_iterator(__unused void** state) { NetplanState* np_state = load_fixture_to_netplan_state("bond.yaml"); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); assert_true(netplan_state_iterator_has_next(&iter)); netdef = netplan_state_iterator_next(&iter); assert_string_equal(netdef->id, "eth0"); assert_true(netplan_state_iterator_has_next(&iter)); netdef = netplan_state_iterator_next(&iter); assert_string_equal(netdef->id, "bond0"); assert_false(netplan_state_iterator_has_next(&iter)); netdef = netplan_state_iterator_next(&iter); assert_null(netdef); netplan_state_clear(&np_state); } void test_netplan_state_iterator_empty(__unused void** state) { NetplanStateIterator iter = { 0 }; NetplanNetDefinition* netdef = NULL; netdef = netplan_state_iterator_next(&iter); assert_null(netdef); } void test_netplan_state_iterator_null(__unused void** state) { NetplanStateIterator *iter = NULL; NetplanNetDefinition* netdef = NULL; netdef = netplan_state_iterator_next(iter); assert_null(netdef); } void test_netplan_state_iterator_null_has_next(__unused void** state) { assert_false(netplan_state_iterator_has_next(NULL)); } int setup(__unused void** state) { return 0; } int tear_down(__unused void** state) { return 0; } int main() { const struct CMUnitTest tests[] = { cmocka_unit_test(test_netplan_state_new_state), cmocka_unit_test(test_netplan_state_iterator), cmocka_unit_test(test_netplan_state_iterator_empty), cmocka_unit_test(test_netplan_state_iterator_null), cmocka_unit_test(test_netplan_state_iterator_null_has_next), }; return cmocka_run_group_tests(tests, setup, tear_down); } netplan-1.0/tests/ctests/test_netplan_validation.c000066400000000000000000000061531457004145200225470ustar00rootroot00000000000000#include #include #include #include #include #include "netplan.h" #include "parse.h" #include "util-internal.h" #include "validation.h" #include "test_utils.h" void test_validate_interface_name_length(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " bridges:\n" " ashortname:\n" " dhcp4: no\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_true(validate_interface_name_length(netdef)); netplan_state_clear(&np_state); } void test_validate_interface_name_length_set_name(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " ethernets:\n" " eth0:\n" " match:\n" " macaddress: aa:bb:cc:dd:ee:ff\n" " set-name: ashortname\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_true(validate_interface_name_length(netdef)); netplan_state_clear(&np_state); } void test_validate_interface_name_length_too_long(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " bridges:\n" " averylongnameforaninterface:\n" " dhcp4: no\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_false(validate_interface_name_length(netdef)); netplan_state_clear(&np_state); } void test_validate_interface_name_length_set_name_too_long(__unused void** state) { const char* yaml = "network:\n" " version: 2\n" " ethernets:\n" " eth0:\n" " match:\n" " macaddress: aa:bb:cc:dd:ee:ff\n" " set-name: averylongnameforaninterface\n"; NetplanState* np_state = load_string_to_netplan_state(yaml); NetplanStateIterator iter; NetplanNetDefinition* netdef = NULL; netplan_state_iterator_init(np_state, &iter); netdef = netplan_state_iterator_next(&iter); assert_false(validate_interface_name_length(netdef)); netplan_state_clear(&np_state); } int setup(__unused void** state) { return 0; } int tear_down(__unused void** state) { return 0; } int main() { const struct CMUnitTest tests[] = { cmocka_unit_test(test_validate_interface_name_length), cmocka_unit_test(test_validate_interface_name_length_too_long), cmocka_unit_test(test_validate_interface_name_length_set_name), cmocka_unit_test(test_validate_interface_name_length_set_name_too_long), }; return cmocka_run_group_tests(tests, setup, tear_down); } netplan-1.0/tests/ctests/test_utils.h000066400000000000000000000035741457004145200200450ustar00rootroot00000000000000#pragma once #include #include "types.h" #include "netplan.h" #include "parse.h" #include "util.h" #include "types-internal.h" gboolean process_document(NetplanParser*, GError**); gboolean load_yaml_from_fd(int, yaml_document_t*, GError**); gboolean load_yaml(const char*, yaml_document_t*, GError**); const char* normalize_ip_address(const char*, const guint); char* write_ovs_bond_interfaces(const NetplanState*, const NetplanNetDefinition*, GString*, GError**); gboolean validate_interface_name_length(const NetplanNetDefinition*); // LCOV_EXCL_START NetplanState * load_fixture_to_netplan_state(const char* filename) { g_autoptr(GError) error = NULL; g_autofree char* filepath = NULL; filepath = g_build_path(G_DIR_SEPARATOR_S, FIXTURESDIR, filename, NULL); NetplanParser *npp = netplan_parser_new(); netplan_parser_load_yaml(npp, filepath, &error); NetplanState *np_state = netplan_state_new(); netplan_state_import_parser_results(np_state, npp, &error); netplan_parser_clear(&npp); return np_state; } NetplanState* load_string_to_netplan_state(const char* yaml) { yaml_parser_t parser; yaml_document_t* doc; NetplanError** error = NULL; NetplanState* np_state = NULL; NetplanParser* npp = netplan_parser_new(); doc = &npp->doc; yaml_parser_initialize(&parser); yaml_parser_set_input_string(&parser, (const unsigned char*) yaml, strlen(yaml)); yaml_parser_load(&parser, doc); process_document(npp, error); if (error && *error) { netplan_error_clear(error); } else { np_state = netplan_state_new(); netplan_state_import_parser_results(np_state, npp, error); } yaml_parser_delete(&parser); yaml_document_delete(doc); netplan_parser_clear(&npp); if (error && *error) { netplan_state_clear(&np_state); } return np_state; } // LCOV_EXCL_STOP netplan-1.0/tests/ctests/test_utils_keyfile.h000066400000000000000000000020021457004145200215360ustar00rootroot00000000000000#pragma once #include #include #include "types.h" #include "netplan.h" #include "parse.h" #include "parse-nm.h" #include "util.h" #include "types-internal.h" // LCOV_EXCL_START NetplanState* load_keyfile_string_to_netplan_state(const char* keyfile) { NetplanError** error = NULL; NetplanState* np_state = NULL; NetplanParser* npp = netplan_parser_new(); int fd = memfd_create("keyfile.nmconnection", 0); char* ptr = (char*) keyfile; while (*ptr) { if (write(fd, ptr, 1) <= 0) break; ptr++; } g_autofree gchar* path = g_strdup_printf("/proc/self/fd/%d", fd); netplan_parser_load_keyfile(npp, path, error); if (error && *error) { netplan_error_clear(error); } else { np_state = netplan_state_new(); netplan_state_import_parser_results(np_state, npp, error); } netplan_parser_clear(&npp); if (error && *error) { netplan_state_clear(&np_state); } return np_state; } // LCOV_EXCL_STOP netplan-1.0/tests/generator/000077500000000000000000000000001457004145200161455ustar00rootroot00000000000000netplan-1.0/tests/generator/__init__.py000066400000000000000000000013201457004145200202520ustar00rootroot00000000000000# # __init__ for generator tests. # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . netplan-1.0/tests/generator/base.py000066400000000000000000000574151457004145200174450ustar00rootroot00000000000000# # Functional tests of netplan generate that verify that the generated # configuration files look as expected. These are run during "make check" and # don't touch the system configuration at all. # # Copyright (C) 2016-2021 Canonical, Ltd. # Author: Martin Pitt # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import random import glob import stat import string import tempfile import subprocess import unittest import ctypes import ctypes.util import yaml import difflib import re from io import StringIO import netplan exe_generate = os.environ.get('NETPLAN_GENERATE_PATH', os.path.join(os.path.dirname(os.path.dirname( os.path.dirname(os.path.abspath(__file__)))), 'generate')) # make sure we point to libnetplan properly. os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))}) # make sure we fail on criticals os.environ['G_DEBUG'] = 'fatal-criticals' lib = ctypes.CDLL('libnetplan.so.1') # common patterns for expected output ND_EMPTY = '[Match]\nName=%s\n\n[Network]\nLinkLocalAddressing=%s\nConfigureWithoutCarrier=yes\n' ND_WITHIP = '[Match]\nName=%s\n\n[Network]\nLinkLocalAddressing=ipv6\nAddress=%s\nConfigureWithoutCarrier=yes\n' ND_WIFI_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=600\nUseMTU=true\n' ND_DHCP = '[Match]\nName=%s\n\n[Network]\nDHCP=%s\nLinkLocalAddressing=ipv6%s\n\n[DHCP]\nRouteMetric=100\nUseMTU=%s\n' ND_DHCP4 = ND_DHCP % ('%s', 'ipv4', '', 'true') ND_DHCP4_NOMTU = ND_DHCP % ('%s', 'ipv4', '', 'false') ND_DHCP6 = ND_DHCP % ('%s', 'ipv6', '', 'true') ND_DHCP6_NOMTU = ND_DHCP % ('%s', 'ipv6', '', 'false') ND_DHCP6_WOCARRIER = ND_DHCP % ('%s', 'ipv6', '\nConfigureWithoutCarrier=yes', 'true') ND_DHCPYES = ND_DHCP % ('%s', 'yes', '', 'true') ND_DHCPYES_NOMTU = ND_DHCP % ('%s', 'yes', '', 'false') _OVS_BASE = '[Unit]\nDescription=OpenVSwitch configuration for %(iface)s\nDefaultDependencies=no\n\ Wants=ovsdb-server.service\nAfter=ovsdb-server.service\n' OVS_PHYSICAL = _OVS_BASE + 'Requires=sys-subsystem-net-devices-%(iface)s.device\nAfter=sys-subsystem-net-devices-%(iface)s\ .device\nAfter=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n%(extra)s' OVS_VIRTUAL = _OVS_BASE + 'After=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n%(extra)s' OVS_BR_DEFAULT = 'ExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan=true\nExecStart=/usr/bin/ovs-vsctl \ set-fail-mode %(iface)s standalone\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan/global/set-fail-mode=\ standalone\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s mcast_snooping_enable=false\nExecStart=/usr/bin/ovs-vsctl set \ Bridge %(iface)s external-ids:netplan/mcast_snooping_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s \ rstp_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan/rstp_enable=false\n' OVS_BR_EMPTY = _OVS_BASE + 'After=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n\n[Service]\n\ Type=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT OVS_CLEANUP = _OVS_BASE + 'ConditionFileIsExecutable=/usr/bin/ovs-vsctl\nBefore=network.target\nWants=network.target\n\n\ [Service]\nType=oneshot\nTimeoutStartSec=10s\nStartLimitBurst=0\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' UDEV_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", ATTR{address}=="%s", NAME="%s"\n' UDEV_NO_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", NAME="%s"\n' UDEV_SRIOV_RULE = 'ACTION=="add", SUBSYSTEM=="net", ATTRS{sriov_totalvfs}=="?*", RUN+="/usr/sbin/netplan apply --sriov-only"\n' ND_WITHIPGW = '[Match]\nName=%s\n\n[Network]\nLinkLocalAddressing=ipv6\nAddress=%s\nAddress=%s\nGateway=%s\n\ ConfigureWithoutCarrier=yes\n' NM_WG = '[connection]\nid=netplan-wg0\ntype=wireguard\ninterface-name=wg0\n\n[wireguard]\nprivate-key=%s\nlisten-port=%s\n%s\ \n\n[ipv4]\nmethod=manual\naddress1=15.15.15.15/24\ngateway=20.20.20.21\n\n[ipv6]\nmethod=manual\naddress1=\ 2001:de:ad:be:ef:ca:fe:1/128\nip6-privacy=0\n' ND_WG = '[NetDev]\nName=wg0\nKind=wireguard\n\n[WireGuard]\nPrivateKey%s\nListenPort=%s\n%s\n' ND_VLAN = '[NetDev]\nName=%s\nKind=vlan\n\n[VLAN]\nId=%d\n' ND_VXLAN = '[NetDev]\nName=%s\nKind=vxlan\n\n[VXLAN]\nVNI=%d\n' ND_VRF = '[NetDev]\nName=%s\nKind=vrf\n\n[VRF]\nTable=%d\n' ND_DUMMY = '[NetDev]\nName=%s\nKind=dummy\n' # wokeignore:rule=dummy ND_VETH = '[NetDev]\nName=%s\nKind=veth\n\n[Peer]\nName=%s\n' SD_WPA = '''[Unit] Description=WPA supplicant for netplan %(iface)s DefaultDependencies=no Requires=sys-subsystem-net-devices-%(iface)s.device After=sys-subsystem-net-devices-%(iface)s.device Before=network.target Wants=network.target [Service] Type=simple ExecStart=/sbin/wpa_supplicant -c /run/netplan/wpa-%(iface)s.conf -i%(iface)s -D%(drivers)s ''' NM_MANAGED = 'SUBSYSTEM=="net", ACTION=="add|change|move", ENV{ID_NET_NAME}=="%s", ENV{NM_UNMANAGED}="0"\n' NM_UNMANAGED = 'SUBSYSTEM=="net", ACTION=="add|change|move", ENV{ID_NET_NAME}=="%s", ENV{NM_UNMANAGED}="1"\n' NM_MANAGED_MAC = 'SUBSYSTEM=="net", ACTION=="add|change|move", ATTR{address}=="%s", ENV{NM_UNMANAGED}="0"\n' NM_UNMANAGED_MAC = 'SUBSYSTEM=="net", ACTION=="add|change|move", ATTR{address}=="%s", ENV{NM_UNMANAGED}="1"\n' NM_MANAGED_DRIVER = 'SUBSYSTEM=="net", ACTION=="add|change|move", ENV{ID_NET_DRIVER}=="%s", ENV{NM_UNMANAGED}="0"\n' NM_UNMANAGED_DRIVER = 'SUBSYSTEM=="net", ACTION=="add|change|move", ENV{ID_NET_DRIVER}=="%s", ENV{NM_UNMANAGED}="1"\n' WOKE_REPLACE_REGEX = ' +# wokeignore:rule=[a-z]+' class NetplanV2Normalizer(): def __init__(self): self.YAML_FALSE = ['n', 'no', 'off', 'false'] self.YAML_TRUE = ['y', 'yes', 'on', 'true'] self.DEFAULT_STANZAS = [ 'dhcp4-overrides: {}', # 2nd level default (containing defaults itself) 'dhcp6-overrides: {}', # 2nd level default (containing defaults itself) 'hidden: false', # access-point 'on-link: false', # route 'stp: true', # paramters 'type: unicast', # route 'version: 2', # global ] self.DEFAULT_NETDEF = { 'dhcp4': self.YAML_FALSE, 'dhcp6': self.YAML_FALSE, 'dhcp-identifier': ['duid'], 'hidden': self.YAML_FALSE, } self.DEFAULT_DHCP = { 'send-hostname': self.YAML_TRUE, 'use-dns': self.YAML_TRUE, 'use-hostname': self.YAML_TRUE, 'use-mtu': self.YAML_TRUE, 'use-ntp': self.YAML_TRUE, 'use-routes': self.YAML_TRUE, } def _clear_mapping_defaults(self, keys, defaults, data): potential_defaults = list(set(keys) & set(defaults.keys())) for k in potential_defaults: if any(map(str(data[k]).lower().__eq__, defaults[k])): del data[k] def normalize_yaml_line(self, line): '''Process formatted YAML line by line (one setting/key per line) Deleting default values and re-writing to default wording ''' kv = line.replace('"', '').replace('\'', '').split(':', 1) if len(kv) != 2 or kv[1].isspace() or kv[1] == '': return line # no normalization needed; no value given # normalize key key = kv[0] if 'gratuitious-arp' in key: # historically supported typo kv[0] = key.replace('gratuitious-arp', 'gratuitous-arp') # normalize value val = kv[1].strip() if val in self.YAML_FALSE: kv[1] = 'false' elif val in self.YAML_TRUE: kv[1] = 'true' elif val == '5G': kv[1] = '5GHz' elif val == '2.4G': kv[1] = '2.4GHz' else: # no normalization needed or known kv[1] = val return ': '.join(kv) def normalize_yaml_tree(self, data, full_key=''): '''Walk the YAML dict/tree @data and sort its sequences in place Keeping track of the @full_key (path), e.g.: "network:ethernets:eth0:dhcp4" And normalizing certain netplan special cases ''' if isinstance(data, list): scalars_only = not any(list(map(lambda elem: (isinstance(elem, dict) or isinstance(elem, list)), data))) # sort sequence alphabetically if scalars_only: data.sort() # remove duplicates (if needed) unique = set(data) if len(data) > len(unique): rm_idx = set() last_idx = 0 for elem in unique: if data.count(elem) > 1: idx = data.index(elem, last_idx) rm_idx.add(idx) last_idx = idx for idx in rm_idx: del data[idx] elif isinstance(data, dict): keys = data.keys() # expand special short forms if 'password' in keys and ':auth' not in full_key: data['auth'] = {'key-management': 'psk', 'password': data['password']} del data['password'] if 'auth' in keys and data['auth'] == {}: data['auth'] = {'key-management': 'none'} # remove default stanza ("link-local: [ ipv6 ]"") if 'link-local' in keys and data['link-local'] == ['ipv6']: del data['link-local'] # remove default stanza ("wakeonwlan: [ default ]") if 'wakeonwlan' in keys and data['wakeonwlan'] == ['default']: del data['wakeonwlan'] # remove explicit openvswitch stanzas, they might not always be # defined in the original YAML (due to being implicit) if ('openvswitch' in keys and data['openvswitch'] == {} and any(map(full_key.__contains__, [':bonds:', ':bridges:', ':vlans:']))): del data['openvswitch'] # remove default empty bond-parameters, those are not rendered by the YAML generator if 'parameters' in keys and data['parameters'] == {} and ':bonds:' in full_key: del data['parameters'] # remove default mode=infrastructore from wifi APs, keeping the SSID if 'mode' in keys and ':wifis:' in full_key and 'infrastructure' in data['mode']: del data['mode'] # ignore renderer: on other than global levels for now, as that # information is currently not stored in the netdef data structure if ('renderer' in keys and len(full_key.split(':')) > 1 and data['renderer'] in ['networkd', 'NetworkManager']): del data['renderer'] # remove default values from the dhcp4/6-overrides mappings if full_key.endswith(':dhcp4-overrides') or full_key.endswith(':dhcp6-overrides'): self._clear_mapping_defaults(keys, self.DEFAULT_DHCP, data) # remove default values from netdef/interface mappings if len(full_key.split(':')) == 3: # netdef level self._clear_mapping_defaults(keys, self.DEFAULT_NETDEF, data) # continue to walk the dict for key in data.keys(): full_key_next = ':'.join([str(full_key), str(key)]) if full_key != '' else key self.normalize_yaml_tree(data[key], full_key_next) def normalize_yaml(self, yaml_dict): # 1st pass: normalize the YAML tree in place, sorting and removing some values self.normalize_yaml_tree(yaml_dict) # 2nd pass: sort the mapping keys and output a formatted yaml (one key per line) formatted_yaml = yaml.dump(yaml_dict, sort_keys=True) # 3rd pass: normalize the wording of certain keys/values per line # and remove any line, containg only default values output = [] for line in formatted_yaml.splitlines(): line = self.normalize_yaml_line(line) if line.strip() in self.DEFAULT_STANZAS: continue output.append(line) return output class TestBase(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() self.confdir = os.path.join(self.workdir.name, 'etc', 'netplan') self.nm_enable_all_conf = os.path.join( self.workdir.name, 'run', 'NetworkManager', 'conf.d', '10-globally-managed-devices.conf') self.maxDiff = None def validate_generated_yaml(self, yaml_input): '''Validate a list of YAML input files one by one. Go through the list @yaml_input one by one, parse the YAML and re-generate the YAML output. Afterwards, normalize and compare the original (and normalized) input with the generated (and normalized) output. ''' for input in yaml_input: parser = netplan.Parser() parser.load_yaml(input) state = netplan.State() state.import_parser_results(parser) # TODO: Allow handling of full hierarchy overrides, # dealing only with the current element of 'yaml_input'. # E.g. allow vlan.id & vlan.link to be defined in a base file. # See test_routing.py: # test_add_routes_to_different_tables_from_multiple_files # test_add_duplicate_routes_from_multiple_files # Read output of the YAML generator (if any) output_fd = StringIO() state._dump_yaml(output_fd) output_yaml = yaml.safe_load(output_fd.getvalue()) # Read input YAML file, as defined by the self.generate('...') method input_yaml = None with open(input, 'r') as orig: input_yaml = yaml.safe_load(orig.read()) # Consider 'network: {}' and 'network: {version: 2}' to be empty if input_yaml is None or input_yaml == {'network': {}} or input_yaml == {'network': {'version': 2}}: input_yaml = yaml.safe_load('') # Normalize input and output YAML netplan_normalizer = NetplanV2Normalizer() input_lines = netplan_normalizer.normalize_yaml(input_yaml) output_lines = netplan_normalizer.normalize_yaml(output_yaml) # Check if (normalized) input and (normalized) output are equal yaml_files_differ = len(input_lines) != len(output_lines) if not yaml_files_differ: # pragma: no cover (only execited in error case) for i in range(len(input_lines)): if input_lines[i] != output_lines[i]: yaml_files_differ = True break if yaml_files_differ: # pragma: no cover (only execited in error case) fromfile = 'original (%s)' % input for line in difflib.unified_diff(input_lines, output_lines, fromfile, tofile='generated', lineterm=''): print(line, flush=True) self.fail('Re-generated YAML file does not match (adopt netplan.c YAML generator?)') def generate(self, yaml, expect_fail=False, extra_args=[], confs=None, skip_generated_yaml_validation=False): '''Call generate with given YAML string as configuration Return stderr output. ''' yaml_input = [] conf = os.path.join(self.confdir, 'a.yaml') os.makedirs(os.path.dirname(conf), exist_ok=True) if yaml is not None: with open(conf, 'w') as f: f.write(yaml) os.chmod(conf, mode=0o600) yaml_input.append(conf) if confs: for f, contents in confs.items(): path = os.path.join(self.confdir, f + '.yaml') with open(path, 'w') as f: f.write(contents) yaml_input.append(path) argv = [exe_generate, '--root-dir', self.workdir.name] + extra_args if 'TEST_SHELL' in os.environ: # pragma nocover print('Test is about to run:\n%s' % ' '.join(argv)) subprocess.call(['bash', '-i'], cwd=self.workdir.name) p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) (out, err) = p.communicate() if expect_fail: self.assertGreater(p.returncode, 0) else: self.assertEqual(p.returncode, 0, err) self.assertEqual(out, '') if not expect_fail and not skip_generated_yaml_validation: yaml_input = list(set(yaml_input + extra_args)) yaml_input.sort() self.validate_generated_yaml(yaml_input) return err def eth_name(self): """Return a link name. Use when you need a link name for a test but don't want to encode a made up name in the test. """ return 'eth' + ''.join(random.sample(string.ascii_letters + string.digits, k=4)) def assert_networkd(self, file_contents_map): networkd_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'network') if not file_contents_map: self.assertFalse(os.path.exists(networkd_dir)) return self.assertEqual(set(os.listdir(self.workdir.name)) - {'lib'}, {'etc', 'run'}) self.assertEqual(set(os.listdir(networkd_dir)), {'10-netplan-' + f for f in file_contents_map}) for fname, contents in file_contents_map.items(): contents = re.sub(WOKE_REPLACE_REGEX, '', contents) with open(os.path.join(networkd_dir, '10-netplan-' + fname)) as f: self.assertEqual(f.read(), contents) def assert_networkd_udev(self, file_contents_map): udev_dir = os.path.join(self.workdir.name, 'run', 'udev', 'rules.d') if not file_contents_map: # it can either not exist, or can only contain 90-netplan.rules self.assertTrue((not os.path.exists(udev_dir)) or (os.listdir(udev_dir) == ['90-netplan.rules'])) return self.assertEqual(set(os.listdir(udev_dir)) - set(['90-netplan.rules']), {'99-netplan-' + f for f in file_contents_map}) for fname, contents in file_contents_map.items(): with open(os.path.join(udev_dir, '99-netplan-' + fname)) as f: self.assertEqual(f.read(), contents) def get_network_config_for_link(self, link_name): """Return the content of the .network file for `link_name`.""" networkd_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'network') with open(os.path.join(networkd_dir, '10-netplan-{}.network'.format(link_name))) as f: return f.read() def get_optional_addresses(self, eth_name): config = self.get_network_config_for_link(eth_name) r = set() prefix = "OptionalAddresses=" for line in config.splitlines(): if line.startswith(prefix): r.add(line[len(prefix):]) return r def assert_wpa_supplicant(self, iface, content): conf_path = os.path.join(self.workdir.name, 'run', 'netplan', "wpa-" + iface + ".conf") with open(conf_path) as f: self.assertEqual(f.read(), content) def assert_nm(self, connections_map=None, conf=None): # check config conf_path = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'conf.d', 'netplan.conf') if conf: conf = re.sub(WOKE_REPLACE_REGEX, '', conf) with open(conf_path) as f: self.assertEqual(f.read(), conf) else: if os.path.exists(conf_path): with open(conf_path) as f: # pragma: nocover self.fail('unexpected %s:\n%s' % (conf_path, f.read())) # check connections con_dir = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'system-connections') if connections_map: self.assertEqual(set(os.listdir(con_dir)), set(['netplan-' + n.split('.nmconnection')[0] + '.nmconnection' for n in connections_map])) for fname, contents in connections_map.items(): contents = re.sub(WOKE_REPLACE_REGEX, '', contents) extension = '' if '.nmconnection' not in fname: extension = '.nmconnection' with open(os.path.join(con_dir, 'netplan-' + fname + extension)) as f: self.assertEqual(f.read(), contents) # NM connection files might contain secrets self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) else: if os.path.exists(con_dir): self.assertEqual(os.listdir(con_dir), []) def assert_nm_udev(self, contents): rule_path = os.path.join(self.workdir.name, 'run/udev/rules.d/90-netplan.rules') if contents is None: self.assertFalse(os.path.exists(rule_path)) return with open(rule_path) as f: lines = [] for line in f.readlines(): # ignore any comment in udev rules.d file if not line.startswith('#'): lines.append(line) self.assertEqual(''.join(lines), contents) def assert_ovs(self, file_contents_map): systemd_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system') if not file_contents_map: # in this case we assume no OVS configuration should be present self.assertFalse(glob.glob(os.path.join(systemd_dir, '*netplan-ovs-*.service'))) return self.assertEqual(set(os.listdir(self.workdir.name)) - {'lib'}, {'etc', 'run'}) ovs_systemd_dir = set(os.listdir(systemd_dir)) ovs_systemd_dir.remove('systemd-networkd.service.wants') self.assertEqual(ovs_systemd_dir, {'netplan-ovs-' + f for f in file_contents_map}) for fname, contents in file_contents_map.items(): fname = 'netplan-ovs-' + fname with open(os.path.join(systemd_dir, fname)) as f: self.assertEqual(f.read(), contents) if fname.endswith('.service'): link_path = os.path.join( systemd_dir, 'systemd-networkd.service.wants', fname) self.assertTrue(os.path.islink(link_path)) link_target = os.readlink(link_path) self.assertEqual(link_target, os.path.join( '/', 'run', 'systemd', 'system', fname)) def assert_sriov(self, file_contents_map): systemd_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system') sriov_systemd_dir = glob.glob(os.path.join(systemd_dir, '*netplan-sriov-*.service')) self.assertEqual(set(os.path.basename(file) for file in sriov_systemd_dir), {'netplan-sriov-' + f for f in file_contents_map}) self.assertEqual(set(os.listdir(self.workdir.name)) - {'lib'}, {'etc', 'run'}) for file in sriov_systemd_dir: basename = os.path.basename(file) with open(file, 'r') as f: contents = f.read() map_contents = file_contents_map.get(basename.replace('netplan-sriov-', '')) self.assertEqual(map_contents, contents) netplan-1.0/tests/generator/test_args.py000066400000000000000000000165051457004145200205210ustar00rootroot00000000000000# # Command-line arguments handling tests for generator # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import subprocess from .base import TestBase, exe_generate, OVS_CLEANUP class TestConfigArgs(TestBase): '''Config file argument handling''' def test_no_files(self): subprocess.check_call([exe_generate, '--root-dir', self.workdir.name]) self.assertEqual(os.listdir(self.workdir.name), ['run']) self.assert_nm_udev(None) def test_no_configs(self): self.generate('network:\n version: 2') # should not write any files self.assertCountEqual(os.listdir(self.workdir.name), ['etc', 'run']) self.assert_networkd(None) self.assert_networkd_udev(None) self.assert_nm(None) self.assert_nm_udev(None) self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) def test_empty_config(self): self.generate('') # should not write any files self.assertCountEqual(os.listdir(self.workdir.name), ['etc', 'run']) self.assert_networkd(None) self.assert_networkd_udev(None) self.assert_nm(None) self.assert_nm_udev(None) self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) def test_file_args(self): conf = os.path.join(self.workdir.name, 'config') with open(conf, 'w') as f: f.write('network: {}') # when specifying custom files, it should ignore the global config self.generate('''network: version: 2 ethernets: eth0: dhcp4: true''', extra_args=[conf]) # There is one systemd service unit 'netplan-ovs-cleanup.service' in /run, # which will always be created self.assertEqual(set(os.listdir(self.workdir.name)), {'config', 'etc', 'run'}) self.assert_networkd(None) self.assert_networkd_udev(None) self.assert_nm(None) self.assert_nm_udev(None) def test_file_args_notfound(self): err = self.generate('''network: version: 2 ethernets: eth0: dhcp4: true''', expect_fail=True, extra_args=['/non/existing/config']) self.assertEqual(err, 'Cannot stat /non/existing/config: No such file or directory\n') self.assertEqual(os.listdir(self.workdir.name), ['etc']) def test_help(self): conf = os.path.join(self.workdir.name, 'etc', 'netplan', 'a.yaml') os.makedirs(os.path.dirname(conf)) with open(conf, 'w') as f: f.write('''network: version: 2 ethernets: eth0: dhcp4: true''') p = subprocess.Popen([exe_generate, '--root-dir', self.workdir.name, '--help'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) (out, err) = p.communicate() self.assertEqual(err, '') self.assertEqual(p.returncode, 0) self.assertIn('Usage:', out) self.assertEqual(os.listdir(self.workdir.name), ['etc']) def test_unknown_cli_args(self): p = subprocess.Popen([exe_generate, '--foo'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) (out, err) = p.communicate() self.assertIn('nknown option --foo', err) self.assertNotEqual(p.returncode, 0) def test_output_mkdir_error(self): conf = os.path.join(self.workdir.name, 'config') with open(conf, 'w') as f: f.write('''network: version: 2 ethernets: eth0: dhcp4: true''') err = self.generate('', extra_args=['--root-dir', '/proc/foo', conf], expect_fail=True) # can be /proc/foor/run/systemd/{network,system} self.assertIn('cannot create directory /proc/foo/run/systemd/', err) def test_systemd_generator(self): conf = os.path.join(self.confdir, 'a.yaml') os.makedirs(os.path.dirname(conf)) with open(conf, 'w') as f: f.write('''network: version: 2 ethernets: eth0: dhcp4: true''') outdir = os.path.join(self.workdir.name, 'out') os.mkdir(outdir) generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') os.makedirs(os.path.dirname(generator)) os.symlink(exe_generate, generator) subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network') self.assertTrue(os.path.exists(n)) os.unlink(n) # should auto-enable networkd and -wait-online self.assertTrue(os.path.islink(os.path.join( outdir, 'multi-user.target.wants', 'systemd-networkd.service'))) self.assertTrue(os.path.islink(os.path.join( outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service'))) # should be a no-op the second time while the stamp exists out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir], stderr=subprocess.STDOUT) self.assertFalse(os.path.exists(n)) self.assertIn(b'netplan generate already ran', out) # after removing the stamp it generates again, and not trip over the # existing enablement symlink os.unlink(os.path.join(outdir, 'netplan.stamp')) subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) self.assertTrue(os.path.exists(n)) def test_systemd_generator_noconf(self): outdir = os.path.join(self.workdir.name, 'out') os.mkdir(outdir) generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') os.makedirs(os.path.dirname(generator)) os.symlink(exe_generate, generator) subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) # no enablement symlink here self.assertEqual(os.listdir(outdir), ['netplan.stamp']) def test_systemd_generator_badcall(self): outdir = os.path.join(self.workdir.name, 'out') os.mkdir(outdir) generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') os.makedirs(os.path.dirname(generator)) os.symlink(exe_generate, generator) try: subprocess.check_output([generator, '--root-dir', self.workdir.name], stderr=subprocess.STDOUT) self.fail("direct systemd generator call is expected to fail, but succeeded.") # pragma: nocover except subprocess.CalledProcessError as e: self.assertEqual(e.returncode, 1) self.assertIn(b'can not be called directly', e.output) netplan-1.0/tests/generator/test_auth.py000066400000000000000000000341371457004145200205270ustar00rootroot00000000000000# # Tests for network authentication config generated via netplan # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import stat from .base import TestBase, ND_DHCP4, ND_WIFI_DHCP4, SD_WPA, NM_MANAGED, NM_UNMANAGED class TestNetworkd(TestBase): def test_auth_wifi_detailed(self): self.generate('''network: version: 2 wifis: wl0: access-points: "Joe's Home": password: "s0s3kr1t" "Luke's Home": auth: key-management: psk password: "4lsos3kr1t" "BobsHome": password: "e03ce667c87bc81ca968d9120ca37f89eb09aec3c55b80386e5d772efd6b926e" "BillsHome": auth: key-management: psk password: "db3b0acf5653aeaddd5fe034fb9f07175b2864f847b005aaa2f09182d9411b04" workplace: auth: key-management: eap method: ttls anonymous-identity: "@internal.example.com" identity: "joe@internal.example.com" password: "v3ryS3kr1t" workplace2: auth: key-management: eap method: peap identity: "joe@internal.example.com" password: "v3ryS3kr1t" ca-certificate: /etc/ssl/work2-cacrt.pem workplacehashed: auth: key-management: eap method: ttls anonymous-identity: "@internal.example.com" identity: "joe@internal.example.com" password: hash:9db1636cedc5948537e7bee0cc1e9590 customernet: auth: key-management: eap method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "d3cryptPr1v4t3K3y" opennet: auth: key-management: none peer2peer: mode: adhoc auth: {} dhcp4: yes ''') self.assert_networkd({'wl0.network': ND_WIFI_DHCP4 % 'wl0'}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'wl0') # generates wpa config and enables wpasupplicant unit with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: new_config = f.read() self.assertIn('ctrl_interface=/run/wpa_supplicant', new_config) self.assertIn(''' network={ ssid="peer2peer" mode=1 key_mgmt=NONE } ''', new_config) self.assertIn(''' network={ ssid="Luke's Home" key_mgmt=WPA-PSK psk="4lsos3kr1t" } ''', new_config) self.assertIn(''' network={ ssid="BobsHome" key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk=e03ce667c87bc81ca968d9120ca37f89eb09aec3c55b80386e5d772efd6b926e } ''', new_config) self.assertIn(''' network={ ssid="BillsHome" key_mgmt=WPA-PSK psk=db3b0acf5653aeaddd5fe034fb9f07175b2864f847b005aaa2f09182d9411b04 } ''', new_config) self.assertIn(''' network={ ssid="workplace2" key_mgmt=WPA-EAP eap=PEAP identity="joe@internal.example.com" password="v3ryS3kr1t" ca_cert="/etc/ssl/work2-cacrt.pem" } ''', new_config) self.assertIn(''' network={ ssid="workplace" key_mgmt=WPA-EAP eap=TTLS identity="joe@internal.example.com" anonymous_identity="@internal.example.com" password="v3ryS3kr1t" } ''', new_config) self.assertIn(''' network={ ssid="workplacehashed" key_mgmt=WPA-EAP eap=TTLS identity="joe@internal.example.com" anonymous_identity="@internal.example.com" password=hash:9db1636cedc5948537e7bee0cc1e9590 } ''', new_config) self.assertIn(''' network={ ssid="customernet" key_mgmt=WPA-EAP eap=TLS identity="cert-joe@cust.example.com" anonymous_identity="@cust.example.com" ca_cert="/etc/ssl/cust-cacrt.pem" client_cert="/etc/ssl/cust-crt.pem" private_key="/etc/ssl/cust-key.pem" private_key_passwd="d3cryptPr1v4t3K3y" } ''', new_config) self.assertIn(''' network={ ssid="opennet" key_mgmt=NONE } ''', new_config) self.assertIn(''' network={ ssid="Joe's Home" key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="s0s3kr1t" } ''', new_config) self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) self.assertTrue(os.path.isfile(os.path.join( self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) self.assertTrue(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) def test_auth_wired(self): self.generate('''network: version: 2 ethernets: eth0: auth: key-management: 802.1x method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "d3cryptPr1v4t3K3y" phase2-auth: MSCHAPV2 dhcp4: yes ''') self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'eth0') # generates wpa config and enables wpasupplicant unit with open(os.path.join(self.workdir.name, 'run/netplan/wpa-eth0.conf')) as f: self.assertEqual(f.read(), '''ctrl_interface=/run/wpa_supplicant network={ key_mgmt=IEEE8021X eap=TLS identity="cert-joe@cust.example.com" anonymous_identity="@cust.example.com" ca_cert="/etc/ssl/cust-cacrt.pem" client_cert="/etc/ssl/cust-crt.pem" private_key="/etc/ssl/cust-key.pem" private_key_passwd="d3cryptPr1v4t3K3y" phase2="auth=MSCHAPV2" } ''') self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) self.assertTrue(os.path.isfile(os.path.join( self.workdir.name, 'run/systemd/system/netplan-wpa-eth0.service'))) with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-eth0.service')) as f: self.assertEqual(f.read(), SD_WPA % {'iface': 'eth0', 'drivers': 'wired'}) self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644) self.assertTrue(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-eth0.service'))) class TestNetworkManager(TestBase): def test_auth_wifi_detailed(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: "Joe's Home": password: "s0s3kr1t" "Luke's Home": auth: key-management: psk password: "4lsos3kr1t" workplace: auth: key-management: eap method: ttls anonymous-identity: "@internal.example.com" identity: "joe@internal.example.com" password: "v3ryS3kr1t" workplace2: auth: key-management: eap method: peap identity: "joe@internal.example.com" password: "v3ryS3kr1t" ca-certificate: /etc/ssl/work2-cacrt.pem workplacehashed: auth: key-management: eap method: ttls anonymous-identity: "@internal.example.com" identity: "joe@internal.example.com" password: hash:9db1636cedc5948537e7bee0cc1e9590 customernet: auth: key-management: 802.1x method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "d3cryptPr1v4t3K3y" phase2-auth: MSCHAPV2 opennet: auth: key-management: none peer2peer: mode: adhoc auth: {} dhcp4: yes ''') self.assert_networkd({}) self.assert_nm({'wl0-Joe%27s%20Home': '''[connection] id=netplan-wl0-Joe's Home type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=Joe's Home mode=infrastructure [wifi-security] key-mgmt=wpa-psk pmf=2 psk=s0s3kr1t ''', 'wl0-Luke%27s%20Home': '''[connection] id=netplan-wl0-Luke's Home type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=Luke's Home mode=infrastructure [wifi-security] key-mgmt=wpa-psk psk=4lsos3kr1t ''', 'wl0-workplace': '''[connection] id=netplan-wl0-workplace type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=workplace mode=infrastructure [wifi-security] key-mgmt=wpa-eap [802-1x] eap=ttls identity=joe@internal.example.com anonymous-identity=@internal.example.com password=v3ryS3kr1t ''', 'wl0-workplace2': '''[connection] id=netplan-wl0-workplace2 type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=workplace2 mode=infrastructure [wifi-security] key-mgmt=wpa-eap [802-1x] eap=peap identity=joe@internal.example.com password=v3ryS3kr1t ca-cert=/etc/ssl/work2-cacrt.pem ''', 'wl0-workplacehashed': '''[connection] id=netplan-wl0-workplacehashed type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=workplacehashed mode=infrastructure [wifi-security] key-mgmt=wpa-eap [802-1x] eap=ttls identity=joe@internal.example.com anonymous-identity=@internal.example.com password=hash:9db1636cedc5948537e7bee0cc1e9590 ''', 'wl0-customernet': '''[connection] id=netplan-wl0-customernet type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=customernet mode=infrastructure [wifi-security] key-mgmt=ieee8021x [802-1x] eap=tls identity=cert-joe@cust.example.com anonymous-identity=@cust.example.com ca-cert=/etc/ssl/cust-cacrt.pem client-cert=/etc/ssl/cust-crt.pem private-key=/etc/ssl/cust-key.pem private-key-password=d3cryptPr1v4t3K3y phase2-auth=MSCHAPV2 ''', 'wl0-opennet': '''[connection] id=netplan-wl0-opennet type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=opennet mode=infrastructure ''', 'wl0-peer2peer': '''[connection] id=netplan-wl0-peer2peer type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=peer2peer mode=adhoc '''}) self.assert_nm_udev(NM_MANAGED % 'wl0') def test_auth_wired(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: auth: key-management: 802.1x method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "d3cryptPr1v4t3K3y" dhcp4: yes ''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=ignore [802-1x] eap=tls identity=cert-joe@cust.example.com anonymous-identity=@cust.example.com ca-cert=/etc/ssl/cust-cacrt.pem client-cert=/etc/ssl/cust-crt.pem private-key=/etc/ssl/cust-key.pem private-key-password=d3cryptPr1v4t3K3y '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eth0') class TestConfigErrors(TestBase): def test_auth_invalid_key_mgmt(self): err = self.generate('''network: version: 2 ethernets: eth0: auth: key-management: bogus''', expect_fail=True) self.assertIn("unknown key management type 'bogus'", err) def test_auth_invalid_eap_method(self): err = self.generate('''network: version: 2 ethernets: eth0: auth: method: bogus''', expect_fail=True) self.assertIn("unknown EAP method 'bogus'", err) def test_auth_networkd_wifi_psk_too_big(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: "Joe's Home": password: "LoremipsumdolorsitametconsecteturadipiscingelitCrastemporvelitnunc" dhcp4: yes''', expect_fail=True) self.assertIn("ASCII passphrase must be between 8 and 63 characters (inclusive)", err) def test_auth_networkd_wifi_psk_too_small(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: "Joe's Home": password: "p4ss" dhcp4: yes''', expect_fail=True) self.assertIn("ASCII passphrase must be between 8 and 63 characters (inclusive)", err) def test_auth_networkd_wifi_psk_64_non_hexdigit(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: "Joe's Home": password: "LoremipsumdolorsitametconsecteturadipiscingelitCrastemporvelitnu" dhcp4: yes''', expect_fail=True) self.assertIn("PSK length of 64 is only supported for hex-digit representation", err) def test_auth_networkd_wire_psk_64_non_hexdigit(self): err = self.generate('''network: version: 2 ethernets: eth0: auth: key-management: psk password: "LoremipsumdolorsitametconsecteturadipiscingelitCrastemporvelitnu" dhcp4: yes''', expect_fail=True) self.assertIn("PSK length of 64 is only supported for hex-digit representation", err) netplan-1.0/tests/generator/test_bonds.py000066400000000000000000000625321457004145200206730ustar00rootroot00000000000000# # Tests for bond devices config generated via netplan # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .base import TestBase, NM_MANAGED, NM_UNMANAGED class TestNetworkd(TestBase): def test_bond_dhcp6_no_accept_ra(self): self.generate('''network: version: 2 ethernets: engreen: dhcp6: no accept-ra: no bonds: bond0: interfaces: [engreen] dhcp6: true accept-ra: yes''') self.assert_networkd({'bond0.network': '''[Match] Name=bond0 [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 IPv6AcceptRA=yes ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'bond0.netdev': '''[NetDev] Name=bond0 Kind=bond ''', 'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=no IPv6AcceptRA=no Bond=bond0 '''}) def test_bond_empty(self): self.generate('''network: version: 2 bonds: bn0: dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', 'bn0.network': '''[Match] Name=bn0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true '''}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'bn0') def test_bond_components(self): self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute bonds: bn0: interfaces: [eno1, switchports] dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', 'bn0.network': '''[Match] Name=bn0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_empty_parameters(self): self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute bonds: bn0: parameters: {} interfaces: [eno1, switchports] dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', 'bn0.network': '''[Match] Name=bn0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_with_parameters_all_members_active(self): self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute bonds: bn0: parameters: mode: 802.3ad lacp-rate: fast mii-monitor-interval: 10 min-links: 10 up-delay: 20 down-delay: 30 all-members-active: true transmit-hash-policy: none ad-select: none arp-interval: 15 arp-validate: all arp-all-targets: all fail-over-mac-policy: none gratuitious-arp: 10 packets-per-member: 10 primary-reselect-policy: none resend-igmp: 10 learn-packet-interval: 10 arp-ip-targets: - 10.10.10.10 - 20.20.20.20 interfaces: [eno1, switchports] dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' '[Bond]\n' 'Mode=802.3ad\n' 'LACPTransmitRate=fast\n' 'MIIMonitorSec=10ms\n' 'MinLinks=10\n' 'TransmitHashPolicy=none\n' 'AdSelect=none\n' 'AllSlavesActive=1\n' # wokeignore:rule=slave 'ARPIntervalSec=15ms\n' 'ARPIPTargets=10.10.10.10 20.20.20.20\n' 'ARPValidate=all\n' 'ARPAllTargets=all\n' 'UpDelaySec=20ms\n' 'DownDelaySec=30ms\n' 'FailOverMACPolicy=none\n' 'GratuitousARP=10\n' 'PacketsPerSlave=10\n' # wokeignore:rule=slave 'PrimaryReselectPolicy=none\n' 'ResendIGMP=10\n' 'LearnPacketIntervalSec=10\n', 'bn0.network': '''[Match] Name=bn0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_with_parameters(self): self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute bonds: bn0: parameters: mode: 802.3ad lacp-rate: fast mii-monitor-interval: 10 min-links: 10 up-delay: 20 down-delay: 30 all-slaves-active: true # wokeignore:rule=slave transmit-hash-policy: none ad-select: none arp-interval: 15 arp-validate: all arp-all-targets: all fail-over-mac-policy: none gratuitious-arp: 10 packets-per-slave: 10 # wokeignore:rule=slave primary-reselect-policy: none resend-igmp: 10 learn-packet-interval: 10 arp-ip-targets: - 10.10.10.10 - 20.20.20.20 interfaces: [eno1, switchports] dhcp4: true''', skip_generated_yaml_validation=True) # Skipping the yaml validation above because the emitter will use # all-members-active and packets-per-member by default. self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' '[Bond]\n' 'Mode=802.3ad\n' 'LACPTransmitRate=fast\n' 'MIIMonitorSec=10ms\n' 'MinLinks=10\n' 'TransmitHashPolicy=none\n' 'AdSelect=none\n' 'AllSlavesActive=1\n' # wokeignore:rule=slave 'ARPIntervalSec=15ms\n' 'ARPIPTargets=10.10.10.10 20.20.20.20\n' 'ARPValidate=all\n' 'ARPAllTargets=all\n' 'UpDelaySec=20ms\n' 'DownDelaySec=30ms\n' 'FailOverMACPolicy=none\n' 'GratuitousARP=10\n' 'PacketsPerSlave=10\n' # wokeignore:rule=slave 'PrimaryReselectPolicy=none\n' 'ResendIGMP=10\n' 'LearnPacketIntervalSec=10\n', 'bn0.network': '''[Match] Name=bn0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_with_parameters_all_suffix(self): self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute bonds: bn0: parameters: mode: 802.3ad mii-monitor-interval: 10ms up-delay: 20ms down-delay: 30s arp-interval: 15m interfaces: [eno1, switchports] dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' '[Bond]\n' 'Mode=802.3ad\n' 'MIIMonitorSec=10ms\n' 'ARPIntervalSec=15m\n' 'UpDelaySec=20ms\n' 'DownDelaySec=30s\n', 'bn0.network': '''[Match] Name=bn0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_primary_member(self): self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute bonds: bn0: parameters: mode: active-backup primary: eno1 interfaces: [eno1, switchports] dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' '[Bond]\n' 'Mode=active-backup\n', 'bn0.network': '''[Match] Name=bn0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n' 'PrimarySlave=true\n', # wokeignore:rule=slave 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) def test_bond_primary_member_duplicate(self): self.generate('''network: version: 2 renderer: networkd ethernets: eno1: {} enp65s0: {} faketh2: {} bonds: bond0: interfaces: [eno1, enp65s0] parameters: primary: enp65s0 mode: balance-tlb vlans: vbr-v10: id: 10 link: vbr bridges: vbr: interfaces: [faketh2]''', expect_fail=False) self.assert_networkd({'eno1.network': '[Match]\nName=eno1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'enp65s0.network': '''[Match] Name=enp65s0 [Network] LinkLocalAddressing=no Bond=bond0 PrimarySlave=true # wokeignore:rule=slave ''', 'faketh2.network': '[Match]\nName=faketh2\n\n[Network]\nLinkLocalAddressing=no\nBridge=vbr\n', 'bond0.network': '[Match]\nName=bond0\n\n' '[Network]\nLinkLocalAddressing=ipv6\nConfigureWithoutCarrier=yes\n', 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n\n[Bond]\nMode=balance-tlb\n', 'vbr-v10.network': '[Match]\nName=vbr-v10\n\n' '[Network]\nLinkLocalAddressing=ipv6\nConfigureWithoutCarrier=yes\n', 'vbr-v10.netdev': '[NetDev]\nName=vbr-v10\nKind=vlan\n\n[VLAN]\nId=10\n', 'vbr.network': '[Match]\nName=vbr\n\n' '[Network]\nLinkLocalAddressing=ipv6\nConfigureWithoutCarrier=yes\nVLAN=vbr-v10\n', 'vbr.netdev': '[NetDev]\nName=vbr\nKind=bridge\n'}) def test_bond_with_gratuitous_spelling(self): """Validate that the correct spelling of gratuitous also works""" self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute bonds: bn0: parameters: mode: active-backup gratuitous-arp: 10 interfaces: [eno1, switchports] dhcp4: true''') self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' '[Bond]\n' 'Mode=active-backup\n' 'GratuitousARP=10\n', 'bn0.network': '''[Match] Name=bn0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) class TestNetworkManager(TestBase): def test_bond_empty(self): self.generate('''network: version: 2 renderer: NetworkManager bonds: bn0: dhcp4: true''') self.assert_nm({'bn0': '''[connection] id=netplan-bn0 type=bond interface-name=bn0 [ipv4] method=auto [ipv6] method=ignore '''}) def test_bond_components(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eno1: {} switchport: match: name: enp2s1 bonds: bn0: interfaces: [eno1, switchport] dhcp4: true''') self.assert_nm({'eno1': '''[connection] id=netplan-eno1 type=ethernet interface-name=eno1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'switchport': '''[connection] id=netplan-switchport type=ethernet interface-name=enp2s1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'bn0': '''[connection] id=netplan-bn0 type=bond interface-name=bn0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eno1' + NM_MANAGED % 'enp2s1' + NM_MANAGED % 'bn0') def test_bond_empty_params(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eno1: {} switchport: match: name: enp2s1 bonds: bn0: interfaces: [eno1, switchport] parameters: {} dhcp4: true''') self.assert_nm({'eno1': '''[connection] id=netplan-eno1 type=ethernet interface-name=eno1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'switchport': '''[connection] id=netplan-switchport type=ethernet interface-name=enp2s1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'bn0': '''[connection] id=netplan-bn0 type=bond interface-name=bn0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eno1' + NM_MANAGED % 'enp2s1' + NM_MANAGED % 'bn0') def test_bond_with_params_all_members_active(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eno1: {} switchport: match: name: enp2s1 bonds: bn0: interfaces: [eno1, switchport] parameters: mode: 802.3ad lacp-rate: slow mii-monitor-interval: 10 min-links: 10 up-delay: 10 down-delay: 10 all-members-active: true transmit-hash-policy: none ad-select: none arp-interval: 10 arp-validate: all arp-all-targets: all arp-ip-targets: - 10.10.10.10 - 20.20.20.20 fail-over-mac-policy: none gratuitious-arp: 10 packets-per-member: 10 primary-reselect-policy: none resend-igmp: 10 learn-packet-interval: 10 dhcp4: true''') self.assert_nm({'eno1': '''[connection] id=netplan-eno1 type=ethernet interface-name=eno1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'switchport': '''[connection] id=netplan-switchport type=ethernet interface-name=enp2s1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'bn0': '''[connection] id=netplan-bn0 type=bond interface-name=bn0 [bond] mode=802.3ad lacp_rate=slow miimon=10 min_links=10 xmit_hash_policy=none ad_select=none all_slaves_active=1 # wokeignore:rule=slave arp_interval=10 arp_ip_target=10.10.10.10,20.20.20.20 arp_validate=all arp_all_targets=all updelay=10 downdelay=10 fail_over_mac=none num_grat_arp=10 num_unsol_na=10 packets_per_slave=10 # wokeignore:rule=slave primary_reselect=none resend_igmp=10 lp_interval=10 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eno1' + NM_MANAGED % 'enp2s1' + NM_MANAGED % 'bn0') def test_bond_with_params(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eno1: {} switchport: match: name: enp2s1 bonds: bn0: interfaces: [eno1, switchport] parameters: mode: 802.3ad lacp-rate: slow mii-monitor-interval: 10 min-links: 10 up-delay: 10 down-delay: 10 all-slaves-active: true # wokeignore:rule=slave transmit-hash-policy: none ad-select: none arp-interval: 10 arp-validate: all arp-all-targets: all arp-ip-targets: - 10.10.10.10 - 20.20.20.20 fail-over-mac-policy: none gratuitious-arp: 10 packets-per-slave: 10 # wokeignore:rule=slave primary-reselect-policy: none resend-igmp: 10 learn-packet-interval: 10 dhcp4: true''', skip_generated_yaml_validation=True) # Skipping the yaml validation above because the emitter will use # all-members-active and packets-per-member by default. self.assert_nm({'eno1': '''[connection] id=netplan-eno1 type=ethernet interface-name=eno1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'switchport': '''[connection] id=netplan-switchport type=ethernet interface-name=enp2s1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'bn0': '''[connection] id=netplan-bn0 type=bond interface-name=bn0 [bond] mode=802.3ad lacp_rate=slow miimon=10 min_links=10 xmit_hash_policy=none ad_select=none all_slaves_active=1 # wokeignore:rule=slave arp_interval=10 arp_ip_target=10.10.10.10,20.20.20.20 arp_validate=all arp_all_targets=all updelay=10 downdelay=10 fail_over_mac=none num_grat_arp=10 num_unsol_na=10 packets_per_slave=10 # wokeignore:rule=slave primary_reselect=none resend_igmp=10 lp_interval=10 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eno1' + NM_MANAGED % 'enp2s1' + NM_MANAGED % 'bn0') def test_bond_primary_member(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eno1: {} switchport: match: name: enp2s1 bonds: bn0: interfaces: [eno1, switchport] parameters: mode: active-backup primary: eno1 dhcp4: true''') self.assert_nm({'eno1': '''[connection] id=netplan-eno1 type=ethernet interface-name=eno1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'switchport': '''[connection] id=netplan-switchport type=ethernet interface-name=enp2s1 slave-type=bond # wokeignore:rule=slave master=bn0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'bn0': '''[connection] id=netplan-bn0 type=bond interface-name=bn0 [bond] mode=active-backup primary=eno1 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eno1' + NM_MANAGED % 'enp2s1' + NM_MANAGED % 'bn0') class TestConfigErrors(TestBase): def test_bond_invalid_mode(self): err = self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bonds: bond0: interfaces: [eno1] parameters: mode: lacp arp-ip-targets: - 2001:dead:beef::1 dhcp4: true''', expect_fail=True) self.assertIn("unknown bond mode 'lacp'", err) def test_bond_invalid_lacp_rate(self): err = self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bonds: bond0: interfaces: [eno1] parameters: lacp-rate: abcd dhcp4: true''', expect_fail=True) self.assertIn("unknown lacp-rate value 'abcd'", err) def test_bond_invalid_arp_target(self): self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bonds: bond0: interfaces: [eno1] parameters: arp-ip-targets: - 2001:dead:beef::1 dhcp4: true''', expect_fail=True) def test_bond_invalid_primary_member(self): self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bonds: bond0: interfaces: [eno1] parameters: primary: wigglewiggle dhcp4: true''', expect_fail=True) def test_bond_duplicate_primary_member(self): self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 eno2: match: name: eth1 bonds: bond0: interfaces: [eno1, eno2] parameters: primary: eno1 primary: eno2 dhcp4: true''', expect_fail=True) def test_bond_multiple_assignments(self): err = self.generate('''network: version: 2 ethernets: eno1: {} bonds: bond0: interfaces: [eno1] bond1: interfaces: [eno1]''', expect_fail=True) self.assertIn("bond1: interface 'eno1' is already assigned to bond bond0", err) def test_bond_bridge_cross_assignments1(self): err = self.generate('''network: version: 2 ethernets: eno1: {} bonds: bond0: interfaces: [eno1] bridges: br1: interfaces: [eno1]''', expect_fail=True) self.assertIn("br1: interface 'eno1' is already assigned to bond bond0", err) def test_bond_bridge_cross_assignments2(self): err = self.generate('''network: version: 2 ethernets: eno1: {} bridges: br0: interfaces: [eno1] bonds: bond1: interfaces: [eno1]''', expect_fail=True) self.assertIn("bond1: interface 'eno1' is already assigned to bridge br0", err) def test_bond_bridge_nested_assignments(self): self.generate('''network: version: 2 ethernets: eno1: {} bonds: bond0: interfaces: [eno1] bridges: br1: interfaces: [bond0]''') netplan-1.0/tests/generator/test_bridges.py000066400000000000000000000357601457004145200212100ustar00rootroot00000000000000# # Tests for bridge devices config generated via netplan # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import unittest from .base import TestBase, NM_UNMANAGED, NM_MANAGED class TestNetworkd(TestBase): def test_bridge_set_mac(self): self.generate('''network: version: 2 bridges: br0: macaddress: 00:01:02:03:04:05 dhcp4: true''') self.assert_networkd({'br0.network': '''[Match] Name=br0 [Link] MACAddress=00:01:02:03:04:05 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'br0.netdev': '[NetDev]\nName=br0\nMACAddress=00:01:02:03:04:05\nKind=bridge\n'}) def test_bridge_dhcp6_no_accept_ra(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: no dhcp6: no accept-ra: no bridges: br0: interfaces: [engreen] dhcp6: true accept-ra: no''') self.assert_networkd({'br0.network': '''[Match] Name=br0 [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 IPv6AcceptRA=no ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'br0.netdev': '''[NetDev] Name=br0 Kind=bridge ''', 'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=no IPv6AcceptRA=no Bridge=br0 '''}) def test_bridge_empty(self): self.generate('''network: version: 2 bridges: br0: dhcp4: true''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', 'br0.network': '''[Match] Name=br0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true '''}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'br0') def test_bridge_type_renderer(self): self.generate('''network: version: 2 renderer: NetworkManager bridges: renderer: networkd br0: dhcp4: true''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', 'br0.network': '''[Match] Name=br0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true '''}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'br0') def test_bridge_def_renderer(self): self.generate('''network: version: 2 renderer: NetworkManager bridges: renderer: NetworkManager br0: renderer: networkd addresses: [1.2.3.4/12] dhcp4: true''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', 'br0.network': '''[Match] Name=br0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 Address=1.2.3.4/12 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true '''}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'br0') def test_bridge_forward_declaration(self): self.generate('''network: version: 2 bridges: br0: interfaces: [eno1, switchports] dhcp4: true ethernets: eno1: {} switchports: match: driver: yayroute ''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', 'br0.network': '''[Match] Name=br0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) @unittest.skipIf("CODECOV_TOKEN" in os.environ, "Skip on codecov.io: GLib changed hashtable sorting") def test_eth_bridge_nm_denylist(self): # pragma: nocover self.generate('''network: renderer: networkd ethernets: eth42: dhcp4: yes ethbr: match: {name: eth43} bridges: mybr: interfaces: [ethbr] dhcp4: yes''') self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'eth42' + NM_UNMANAGED % 'eth43' + NM_UNMANAGED % 'mybr') def test_bridge_components(self): self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute bridges: br0: interfaces: [eno1, switchports] dhcp4: true''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', 'br0.network': '''[Match] Name=br0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) def test_bridge_params(self): self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute bridges: br0: interfaces: [eno1, switchports] parameters: ageing-time: 50 forward-delay: 12 hello-time: 6 max-age: 24 priority: 1000 stp: true path-cost: eno1: 70 port-priority: eno1: 14 dhcp4: true''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n' '[Bridge]\nAgeingTimeSec=50\n' 'Priority=1000\n' 'ForwardDelaySec=12\n' 'HelloTimeSec=6\n' 'MaxAgeSec=24\n' 'STP=true\n', 'br0.network': '''[Match] Name=br0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n' '[Bridge]\nCost=70\nPriority=14\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) class TestNetworkManager(TestBase): def test_bridge_empty(self): self.generate('''network: version: 2 renderer: NetworkManager bridges: br0: dhcp4: true''') self.assert_nm({'br0': '''[connection] id=netplan-br0 type=bridge interface-name=br0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'br0') def test_bridge_type_renderer(self): self.generate('''network: version: 2 renderer: networkd bridges: renderer: NetworkManager br0: dhcp4: true''') self.assert_nm({'br0': '''[connection] id=netplan-br0 type=bridge interface-name=br0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'br0') def test_bridge_set_mac(self): self.generate('''network: version: 2 bridges: renderer: NetworkManager br0: macaddress: 00:01:02:03:04:05 dhcp4: true''') self.assert_nm({'br0': '''[connection] id=netplan-br0 type=bridge interface-name=br0 [ethernet] cloned-mac-address=00:01:02:03:04:05 [ipv4] method=auto [ipv6] method=ignore '''}) def test_bridge_def_renderer(self): self.generate('''network: version: 2 renderer: networkd bridges: renderer: networkd br0: renderer: NetworkManager addresses: [1.2.3.4/12] dhcp4: true''') self.assert_nm({'br0': '''[connection] id=netplan-br0 type=bridge interface-name=br0 [ipv4] method=auto address1=1.2.3.4/12 [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'br0') def test_bridge_forward_declaration(self): self.generate('''network: version: 2 renderer: NetworkManager bridges: br0: interfaces: [eno1, switchport] dhcp4: true ethernets: eno1: {} switchport: match: name: enp2s1 ''') self.assert_nm({'eno1': '''[connection] id=netplan-eno1 type=ethernet interface-name=eno1 slave-type=bridge # wokeignore:rule=slave master=br0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'switchport': '''[connection] id=netplan-switchport type=ethernet interface-name=enp2s1 slave-type=bridge # wokeignore:rule=slave master=br0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'br0': '''[connection] id=netplan-br0 type=bridge interface-name=br0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'br0' + NM_MANAGED % 'eno1' + NM_MANAGED % 'enp2s1') def test_bridge_components(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eno1: {} switchport: match: name: enp2s1 bridges: br0: interfaces: [eno1, switchport] dhcp4: true''') self.assert_nm({'eno1': '''[connection] id=netplan-eno1 type=ethernet interface-name=eno1 slave-type=bridge # wokeignore:rule=slave master=br0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'switchport': '''[connection] id=netplan-switchport type=ethernet interface-name=enp2s1 slave-type=bridge # wokeignore:rule=slave master=br0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'br0': '''[connection] id=netplan-br0 type=bridge interface-name=br0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eno1' + NM_MANAGED % 'enp2s1' + NM_MANAGED % 'br0') def test_bridge_params(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eno1: {} switchport: match: name: enp2s1 bridges: br0: interfaces: [eno1, switchport] parameters: ageing-time: 50 priority: 1000 forward-delay: 12 hello-time: 6 max-age: 24 path-cost: eno1: 70 port-priority: eno1: 61 stp: true dhcp4: true''') self.assert_nm({'eno1': '''[connection] id=netplan-eno1 type=ethernet interface-name=eno1 slave-type=bridge # wokeignore:rule=slave master=br0 # wokeignore:rule=master [bridge-port] path-cost=70 priority=61 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'switchport': '''[connection] id=netplan-switchport type=ethernet interface-name=enp2s1 slave-type=bridge # wokeignore:rule=slave master=br0 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'br0': '''[connection] id=netplan-br0 type=bridge interface-name=br0 [bridge] ageing-time=50 priority=1000 forward-delay=12 hello-time=6 max-age=24 stp=true [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eno1' + NM_MANAGED % 'enp2s1' + NM_MANAGED % 'br0') class TestNetplanYAMLv2(TestBase): '''No asserts are needed. The generate() method implicitly checks the (re-)generated YAML. ''' def test_bridge_stp(self): self.generate('''network: version: 2 bridges: br0: parameters: stp: no dhcp4: true''') class TestConfigErrors(TestBase): def test_bridge_unknown_iface(self): err = self.generate('''network: version: 2 bridges: br0: interfaces: ['foo']''', expect_fail=True) self.assertIn("br0: interface 'foo' is not defined", err) def test_bridge_multiple_assignments(self): err = self.generate('''network: version: 2 ethernets: eno1: {} bridges: br0: interfaces: [eno1] br1: interfaces: [eno1]''', expect_fail=True) self.assertIn("br1: interface 'eno1' is already assigned to bridge br0", err) def test_bridge_invalid_dev_for_path_cost(self): self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bridges: br0: interfaces: [eno1] parameters: path-cost: eth0: 50 dhcp4: true''', expect_fail=True) def test_bridge_path_cost_already_defined(self): self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bridges: br0: interfaces: [eno1] parameters: path-cost: eno1: 50 eno1: 40 dhcp4: true''', expect_fail=True) def test_bridge_invalid_path_cost(self): self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bridges: br0: interfaces: [eno1] parameters: path-cost: eno1: aa dhcp4: true''', expect_fail=True) def test_bridge_invalid_dev_for_port_prio(self): self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bridges: br0: interfaces: [eno1] parameters: port-priority: eth0: 50 dhcp4: true''', expect_fail=True) def test_bridge_port_prio_already_defined(self): self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bridges: br0: interfaces: [eno1] parameters: port-priority: eno1: 50 eno1: 40 dhcp4: true''', expect_fail=True) def test_bridge_invalid_port_prio(self): self.generate('''network: version: 2 ethernets: eno1: match: name: eth0 bridges: br0: interfaces: [eno1] parameters: port-priority: eno1: 257 dhcp4: true''', expect_fail=True) netplan-1.0/tests/generator/test_common.py000066400000000000000000001140411457004145200210470ustar00rootroot00000000000000# # Common tests for netplan generator # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import textwrap from .base import TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, ND_EMPTY, NM_MANAGED, NM_UNMANAGED class TestNetworkd(TestBase): '''networkd output''' def test_optional(self): self.generate('''network: version: 2 ethernets: eth0: dhcp6: true optional: true''') self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Link] RequiredForOnline=no [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) self.assert_networkd_udev(None) def config_with_optional_addresses(self, eth_name, optional_addresses): return '''network: version: 2 ethernets: {}: dhcp6: true optional-addresses: {}'''.format(eth_name, optional_addresses) def test_optional_addresses(self): eth_name = self.eth_name() self.generate(self.config_with_optional_addresses(eth_name, '["dhcp4"]')) self.assertEqual(self.get_optional_addresses(eth_name), set(["dhcp4"])) def test_optional_addresses_multiple(self): eth_name = self.eth_name() self.generate(self.config_with_optional_addresses(eth_name, '[dhcp4, ipv4-ll, ipv6-ra, dhcp6, dhcp4, static]')) self.assertEqual( self.get_optional_addresses(eth_name), set(["ipv4-ll", "ipv6-ra", "dhcp4", "dhcp6", "static"])) def test_optional_addresses_invalid(self): eth_name = self.eth_name() err = self.generate(self.config_with_optional_addresses(eth_name, '["invalid"]'), expect_fail=True) self.assertIn('invalid value for optional-addresses', err) def test_activation_mode_off(self): self.generate('''network: version: 2 ethernets: eth0: dhcp6: true activation-mode: off''') self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Link] ActivationPolicy=always-down RequiredForOnline=no [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) self.assert_networkd_udev(None) def test_activation_mode_manual(self): self.generate('''network: version: 2 ethernets: eth0: dhcp6: true activation-mode: manual''') self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Link] ActivationPolicy=manual RequiredForOnline=no [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) self.assert_networkd_udev(None) def test_mtu_all(self): self.generate(textwrap.dedent(""" network: version: 2 ethernets: eth1: mtu: 9000 dhcp4: n ipv6-mtu: 2000 bonds: bond0: interfaces: - eth1 mtu: 9000 vlans: bond0.108: link: bond0 id: 108""")) self.assert_networkd({ 'bond0.108.netdev': '[NetDev]\nName=bond0.108\nKind=vlan\n\n[VLAN]\nId=108\n', 'bond0.108.network': '''[Match] Name=bond0.108 [Network] LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes ''', 'bond0.netdev': '[NetDev]\nName=bond0\nMTUBytes=9000\nKind=bond\n', 'bond0.network': '''[Match] Name=bond0 [Link] MTUBytes=9000 [Network] LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes VLAN=bond0.108 ''', 'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=9000\n', 'eth1.network': '''[Match] Name=eth1 [Link] MTUBytes=9000 [Network] LinkLocalAddressing=no IPv6MTUBytes=2000 Bond=bond0 ''' }) self.assert_networkd_udev(None) def test_eth_global_renderer(self): self.generate('''network: version: 2 renderer: networkd ethernets: eth0: dhcp4: true''') self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'eth0') # should not allow NM to manage everything self.assertFalse(os.path.exists(self.nm_enable_all_conf)) def test_eth_type_renderer(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: renderer: networkd eth0: dhcp4: true''') self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) self.assert_nm(None) # should allow NM to manage everything else self.assertTrue(os.path.exists(self.nm_enable_all_conf)) self.assert_nm_udev(NM_UNMANAGED % 'eth0') def test_eth_def_renderer(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: renderer: NetworkManager eth0: renderer: networkd dhcp4: true''') self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) self.assert_networkd_udev(None) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'eth0') def test_eth_dhcp6(self): self.generate('''network: version: 2 ethernets: eth0: {dhcp6: true}''') self.assert_networkd({'eth0.network': ND_DHCP6 % 'eth0'}) def test_eth_dhcp6_no_accept_ra(self): self.generate('''network: version: 2 ethernets: eth0: dhcp6: true accept-ra: n''') self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 IPv6AcceptRA=no [DHCP] RouteMetric=100 UseMTU=true '''}) def test_eth_dhcp6_accept_ra(self): self.generate('''network: version: 2 ethernets: eth0: dhcp6: true accept-ra: yes''') self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 IPv6AcceptRA=yes [DHCP] RouteMetric=100 UseMTU=true '''}) def test_eth_dhcp6_accept_ra_unset(self): self.generate('''network: version: 2 ethernets: eth0: dhcp6: true''') self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) def test_eth_dhcp4_and_6(self): self.generate('''network: version: 2 ethernets: eth0: {dhcp4: true, dhcp6: true}''') self.assert_networkd({'eth0.network': ND_DHCPYES % 'eth0'}) def test_eth_manual_addresses(self): self.generate('''network: version: 2 ethernets: engreen: addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 Address=2001:FFfe::1/64 '''}) def test_eth_manual_addresses_dhcp(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 Address=192.168.14.2/24 Address=2001:FFfe::1/64 [DHCP] RouteMetric=100 UseMTU=true '''}) def test_eth_address_option_lifetime_zero(self): self.generate('''network: version: 2 ethernets: engreen: addresses: - 192.168.14.2/24: lifetime: 0 - 2001:FFfe::1/64''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=2001:FFfe::1/64 [Address] Address=192.168.14.2/24 PreferredLifetime=0 '''}) def test_eth_address_option_lifetime_forever(self): self.generate('''network: version: 2 ethernets: engreen: addresses: - 192.168.14.2/24: lifetime: forever - 2001:FFfe::1/64''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=2001:FFfe::1/64 [Address] Address=192.168.14.2/24 PreferredLifetime=forever '''}) def test_eth_address_option_label(self): self.generate('''network: version: 2 ethernets: engreen: addresses: - 192.168.14.2/24: label: test-label - 2001:FFfe::1/64''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=2001:FFfe::1/64 [Address] Address=192.168.14.2/24 Label=test-label '''}) def test_eth_address_option_multi_pass(self): self.generate('''network: version: 2 bridges: br0: interfaces: [engreen] ethernets: engreen: addresses: - 192.168.14.2/24: label: test-label - 2001:FFfe::1/64: label: ip6''') self.assert_networkd({ 'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=no Bridge=br0 [Address] Address=192.168.14.2/24 Label=test-label [Address] Address=2001:FFfe::1/64 Label=ip6 ''', 'br0.network': ND_EMPTY % ('br0', 'ipv6'), 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n'}) def test_bond_arp_ip_targets_multi_pass(self): self.generate('''network: bonds: bond0: interfaces: - eno1 parameters: arp-ip-targets: - 10.10.10.10 - 20.20.20.20 ethernets: eno1: {} version: 2''') self.assert_networkd({'bond0.netdev': '''[NetDev] Name=bond0 Kind=bond [Bond] ARPIPTargets=10.10.10.10 20.20.20.20 ''', 'bond0.network': '''[Match] Name=bond0 [Network] LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes ''', 'eno1.network': '''[Match] Name=eno1 [Network] LinkLocalAddressing=no Bond=bond0 '''}) def test_dhcp_critical_true(self): self.generate('''network: version: 2 ethernets: engreen: critical: yes ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 KeepConfiguration=true '''}) def test_dhcp_identifier_mac(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp-identifier: mac ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [DHCP] ClientIdentifier=mac RouteMetric=100 UseMTU=true '''}) def test_dhcp_identifier_duid(self): # This option should be silently ignored, since it's the default self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp-identifier: duid ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) def test_eth_ipv6_privacy(self): self.generate('''network: version: 2 ethernets: eth0: dhcp6: true ipv6-privacy: true''') self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 IPv6PrivacyExtensions=yes [DHCP] RouteMetric=100 UseMTU=true '''}) def test_eth_ignore_carrier_true(self): self.generate('''network: version: 2 ethernets: eth0: ignore-carrier: yes ''') self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Network] LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes '''}) def test_gateway4(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] gateway4: 192.168.14.1''') self.assertIn("`gateway4` has been deprecated, use default routes instead.", err) self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 Gateway=192.168.14.1 '''}) def test_gateway6(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: ["2001:FFfe::1/64"] gateway6: 2001:FFfe::2''') self.assertIn("`gateway6` has been deprecated, use default routes instead.", err) self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=2001:FFfe::1/64 Gateway=2001:FFfe::2 '''}) def test_gateway_full(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] gateway4: 192.168.14.1 gateway6: "2001:FFfe::2"''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 Address=2001:FFfe::1/64 Gateway=192.168.14.1 Gateway=2001:FFfe::2 '''}) def test_gateways_multi_pass(self): self.generate('''network: version: 2 bridges: br0: interfaces: [engreen] ethernets: engreen: addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] gateway4: 192.168.14.1 gateway6: "2001:FFfe::2"''') self.assert_networkd({ 'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=no Address=192.168.14.2/24 Address=2001:FFfe::1/64 Gateway=192.168.14.1 Gateway=2001:FFfe::2 Bridge=br0 ''', 'br0.network': ND_EMPTY % ('br0', 'ipv6'), 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n'}) def test_nameserver(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] nameservers: addresses: [1.2.3.4, "1234::FFFF"] enblue: addresses: ["192.168.1.3/24"] nameservers: search: [lab, kitchen] addresses: [8.8.8.8]''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 DNS=1.2.3.4 DNS=1234::FFFF ''', 'enblue.network': '''[Match] Name=enblue [Network] LinkLocalAddressing=ipv6 Address=192.168.1.3/24 DNS=8.8.8.8 Domains=lab kitchen '''}) def test_link_local_all(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp6: yes link-local: [ ipv4, ipv6 ] ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=yes LinkLocalAddressing=yes [DHCP] RouteMetric=100 UseMTU=true '''}) def test_link_local_ipv4(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp6: yes link-local: [ ipv4 ] ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=yes LinkLocalAddressing=ipv4 [DHCP] RouteMetric=100 UseMTU=true '''}) def test_link_local_ipv6(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp6: yes link-local: [ ipv6 ] ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=yes LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) def test_link_local_disabled(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp6: yes link-local: [ ] ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=yes LinkLocalAddressing=no [DHCP] RouteMetric=100 UseMTU=true '''}) def test_ip6_addr_gen_mode(self): self.generate('''network: version: 2 renderer: networkd ethernets: enblue: dhcp6: yes ipv6-address-generation: eui64''') self.assert_networkd({'enblue.network': '''[Match]\nName=enblue\n [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) def test_ip6_addr_gen_token(self): self.generate('''network: version: 2 renderer: networkd ethernets: engreen: dhcp6: yes ipv6-address-token: ::2 enblue: dhcp6: yes ipv6-address-token: "::2"''') self.assert_networkd({'engreen.network': '''[Match]\nName=engreen\n [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 IPv6Token=static:::2 [DHCP] RouteMetric=100 UseMTU=true ''', 'enblue.network': '''[Match]\nName=enblue\n [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 IPv6Token=static:::2 [DHCP] RouteMetric=100 UseMTU=true '''}) class TestNetworkManager(TestBase): def test_empty_conf(self): self.generate('''network: version: 2 renderer: NetworkManager''') self.assert_nm({}) def test_mtu_all(self): self.generate(textwrap.dedent(""" network: version: 2 renderer: NetworkManager ethernets: eth1: mtu: 1280 dhcp4: n bonds: bond0: interfaces: - eth1 mtu: 9000 vlans: bond0.108: link: bond0 id: 108""")) self.assert_nm({ 'bond0.108': '''[connection] id=netplan-bond0.108 type=vlan interface-name=bond0.108 [vlan] id=108 parent=bond0 [ipv4] method=link-local [ipv6] method=ignore ''', 'bond0': '''[connection] id=netplan-bond0 type=bond interface-name=bond0 [ethernet] mtu=9000 [ipv4] method=link-local [ipv6] method=ignore ''', 'eth1': '''[connection] id=netplan-eth1 type=ethernet interface-name=eth1 slave-type=bond # wokeignore:rule=slave master=bond0 # wokeignore:rule=master [ethernet] wake-on-lan=0 mtu=1280 [ipv4] method=link-local [ipv6] method=ignore ''', }) def test_activation_mode_off(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: activation-mode: off''', expect_fail=True) def test_activation_mode_manual(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: activation-mode: manual''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet autoconnect=false interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eth0') def test_ipv6_mtu(self): self.generate(textwrap.dedent(""" network: version: 2 renderer: NetworkManager ethernets: eth1: mtu: 9000 ipv6-mtu: 2000"""), expect_fail=True) def test_eth_global_renderer(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: dhcp4: true''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eth0') def test_eth_type_renderer(self): self.generate('''network: version: 2 renderer: networkd ethernets: renderer: NetworkManager eth0: dhcp4: true''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eth0') def test_eth_def_renderer(self): self.generate('''network: version: 2 renderer: networkd ethernets: renderer: networkd eth0: renderer: NetworkManager''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'eth0') def test_global_renderer_only(self): self.generate(None, confs={'01-default-nm.yaml': 'network: {version: 2, renderer: NetworkManager}'}) # should allow NM to manage everything else self.assertTrue(os.path.exists(self.nm_enable_all_conf)) # but not configure anything else self.assert_nm(None, None) self.assert_networkd({}) self.assert_nm_udev(None) def test_eth_dhcp6(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: {dhcp6: true}''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=auto ip6-privacy=0 '''}) def test_eth_dhcp4_and_6(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: {dhcp4: true, dhcp6: true}''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=auto ip6-privacy=0 '''}) def test_ip6_addr_gen_mode(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp6: yes ipv6-address-generation: stable-privacy enblue: dhcp6: yes ipv6-address-generation: eui64''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=auto addr-gen-mode=1 ip6-privacy=0 ''', 'enblue': '''[connection] id=netplan-enblue type=ethernet interface-name=enblue [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=auto addr-gen-mode=0 ip6-privacy=0 '''}) def test_ip6_addr_gen_token(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp6: yes ipv6-address-token: ::2 enblue: dhcp6: yes ipv6-address-token: "::2"''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=auto addr-gen-mode=0 token=::2 ip6-privacy=0 ''', 'enblue': '''[connection] id=netplan-enblue type=ethernet interface-name=enblue [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=auto addr-gen-mode=0 token=::2 ip6-privacy=0 '''}) def test_eth_manual_addresses(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: - 192.168.14.2/24 - 172.16.0.4/16 - 2001:FFfe::1/64''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 address2=172.16.0.4/16 [ipv6] method=manual address1=2001:FFfe::1/64 ip6-privacy=0 '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'engreen') def test_eth_manual_addresses_dhcp(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp4: yes addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=auto address1=192.168.14.2/24 [ipv6] method=manual address1=2001:FFfe::1/64 ip6-privacy=0 '''}) def test_eth_ipv6_privacy(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: {dhcp6: true, ipv6-privacy: true}''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=auto ip6-privacy=2 '''}) def test_gateway(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] gateway4: 192.168.14.1 gateway6: 2001:FFfe::2''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 gateway=192.168.14.1 [ipv6] method=manual address1=2001:FFfe::1/64 ip6-privacy=0 gateway=2001:FFfe::2 '''}) def test_nameserver(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: ["192.168.14.2/24"] nameservers: addresses: [1.2.3.4, 2.3.4.5, "1234::FFFF"] search: [lab, kitchen] enblue: addresses: ["192.168.1.3/24"] nameservers: addresses: [8.8.8.8]''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 dns=1.2.3.4;2.3.4.5; dns-search=lab;kitchen; [ipv6] method=manual ip6-privacy=0 dns=1234::FFFF; dns-search=lab;kitchen; ''', 'enblue': '''[connection] id=netplan-enblue type=ethernet interface-name=enblue [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.1.3/24 dns=8.8.8.8; [ipv6] method=ignore '''}) class TestForwardDeclaration(TestBase): def test_fwdecl_bridge_on_bond(self): self.generate('''network: version: 2 bridges: br0: interfaces: ['bond0'] dhcp4: true bonds: bond0: interfaces: ['eth0', 'eth1'] ethernets: eth0: match: macaddress: 00:01:02:03:04:05 set-name: eth0 eth1: match: macaddress: 02:01:02:03:04:05 set-name: eth1 ''') self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', 'br0.network': '''[Match] Name=br0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n', 'bond0.network': '''[Match] Name=bond0 [Network] LinkLocalAddressing=no ConfigureWithoutCarrier=yes Bridge=br0 ''', 'eth0.link': '''[Match] PermanentMACAddress=00:01:02:03:04:05 [Link] Name=eth0 WakeOnLan=off ''', 'eth0.network': '''[Match] PermanentMACAddress=00:01:02:03:04:05 Name=eth0 [Network] LinkLocalAddressing=no Bond=bond0 ''', 'eth1.link': '''[Match] PermanentMACAddress=02:01:02:03:04:05 [Link] Name=eth1 WakeOnLan=off ''', 'eth1.network': '''[Match] PermanentMACAddress=02:01:02:03:04:05 Name=eth1 [Network] LinkLocalAddressing=no Bond=bond0 '''}) def test_fwdecl_feature_blend(self): self.generate('''network: version: 2 vlans: vlan1: link: 'br0' id: 1 dhcp4: true bridges: br0: interfaces: ['bond0', 'eth2'] parameters: path-cost: eth2: 1000 bond0: 8888 bonds: bond0: interfaces: ['eth0', 'br1'] ethernets: eth0: match: macaddress: 00:01:02:03:04:05 set-name: eth0 bridges: br1: interfaces: ['eth1'] ethernets: eth1: match: macaddress: 02:01:02:03:04:05 set-name: eth1 eth2: match: name: eth2 ''', skip_generated_yaml_validation=True) # XXX: We need to skeip the generated YAML validation, as the pyYAML # parser overrides the duplicate "ethernets"/"bridges" keys, while # the netplan C YAML parser merges them into the netdef self.assert_networkd({'vlan1.netdev': '[NetDev]\nName=vlan1\nKind=vlan\n\n' '[VLAN]\nId=1\n', 'vlan1.network': '''[Match] Name=vlan1 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n' '[Bridge]\nSTP=true\n', 'br0.network': '''[Match] Name=br0 [Network] LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes VLAN=vlan1 ''', 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n', 'bond0.network': '''[Match] Name=bond0 [Network] LinkLocalAddressing=no ConfigureWithoutCarrier=yes Bridge=br0 [Bridge] Cost=8888 ''', 'eth2.network': '[Match]\nName=eth2\n\n' '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n' '[Bridge]\nCost=1000\n', 'br1.netdev': '[NetDev]\nName=br1\nKind=bridge\n', 'br1.network': '''[Match] Name=br1 [Network] LinkLocalAddressing=no ConfigureWithoutCarrier=yes Bond=bond0 ''', 'eth0.link': '''[Match] PermanentMACAddress=00:01:02:03:04:05 [Link] Name=eth0 WakeOnLan=off ''', 'eth0.network': '''[Match] PermanentMACAddress=00:01:02:03:04:05 Name=eth0 [Network] LinkLocalAddressing=no Bond=bond0 ''', 'eth1.link': '''[Match] PermanentMACAddress=02:01:02:03:04:05 [Link] Name=eth1 WakeOnLan=off ''', 'eth1.network': '''[Match] PermanentMACAddress=02:01:02:03:04:05 Name=eth1 [Network] LinkLocalAddressing=no Bridge=br1 '''}) def test_fwdecl_will_not_lead_to_duplicates(self): ''' When the parser needs more than one pass we shouldn't emit duplicated configuration Testcase for LP: #2007682''' self.generate('''network: bonds: aggi: routing-policy: - table: 1 from: 1.2.3.4 nameservers: addresses: - 1.2.3.4 search: - example.com interfaces: - eth0 ethernets: eth0: ignore-carrier: true ''') self.assert_networkd({'aggi.netdev': '[NetDev]\nName=aggi\nKind=bond\n', 'aggi.network': '''[Match] Name=aggi [Network] LinkLocalAddressing=ipv6 DNS=1.2.3.4 Domains=example.com ConfigureWithoutCarrier=yes [RoutingPolicyRule] From=1.2.3.4 Table=1 ''', 'eth0.network': ND_EMPTY % ('eth0', 'no') + 'Bond=aggi\n'}) def test_check_parser_second_pass_will_not_lead_to_duplicate_access_point(self): ''' When the parser needs more than one pass we shouldn't try to load the same access-point again from wifi devices. Testcase for LP: #1809994''' self.generate('''network: bridges: br0: interfaces: - eth0 ethernets: eth0: dhcp4: false wifis: wlan0: dhcp4: true access-points: "mywifi": password: "aaaaaaaa" ''') class TestMerging(TestBase): '''multiple *.yaml merging''' def test_global_backend(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp4: y''', confs={'backend': 'network:\n renderer: networkd'}) self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'engreen') def test_add_def(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: true''', confs={'blue': '''network: version: 2 ethernets: enblue: dhcp4: true'''}) self.assert_networkd({'enblue.network': ND_DHCP4 % 'enblue', 'engreen.network': ND_DHCP4 % 'engreen'}) # Skip on codecov.io; GLib changed hashtable elements ordering between # releases, so we can't depend on the exact order. # TODO: (cyphermox) turn this into an "assert_in_nm()" function. if "CODECOV_TOKEN" not in os.environ: # pragma: nocover self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'engreen' + NM_UNMANAGED % 'enblue') def test_change_def(self): self.generate('''network: version: 2 ethernets: engreen: wakeonlan: true dhcp4: false''', confs={'green-dhcp': '''network: version: 2 ethernets: engreen: dhcp4: true'''}) self.assert_networkd({'engreen.link': '[Match]\nOriginalName=engreen\n\n[Link]\nWakeOnLan=magic\n', 'engreen.network': ND_DHCP4 % 'engreen'}) def test_cleanup_old_config(self): self.generate('''network: version: 2 ethernets: engreen: {dhcp4: true} enyellow: {renderer: NetworkManager}''', confs={'blue': '''network: version: 2 ethernets: enblue: dhcp4: true'''}) os.unlink(os.path.join(self.confdir, 'blue.yaml')) self.generate('''network: version: 2 ethernets: engreen: {dhcp4: true}''') self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'engreen') def test_ref(self): self.generate('''network: version: 2 ethernets: eno1: {} switchports: match: driver: yayroute''', confs={'bridges': '''network: version: 2 bridges: br0: interfaces: [eno1, switchports] dhcp4: true'''}, skip_generated_yaml_validation=True) # XXX: We need to skip the generated YAML validation, as the 'bridges' # conf is invalid in itself (missing eno1 & switchports defs) and # can only be parsed if merged with the main YAML self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', 'br0.network': '''[Match] Name=br0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes [DHCP] RouteMetric=100 UseMTU=true ''', 'eno1.network': '[Match]\nName=eno1\n\n' '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'switchports.network': '[Match]\nDriver=yayroute\n\n' '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) def test_def_in_run(self): rundir = os.path.join(self.workdir.name, 'run', 'netplan') os.makedirs(rundir) # override b.yaml definition for enred with open(os.path.join(rundir, 'b.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: {enred: {dhcp4: true}}''') # append new definition for enblue with open(os.path.join(rundir, 'c.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: {enblue: {dhcp4: true}}''') self.generate('''network: version: 2 ethernets: engreen: {dhcp4: true}''', confs={'b': '''network: version: 2 ethernets: {enred: {wakeonlan: true}}'''}) # b.yaml in /run/ should completely shadow b.yaml in /etc, thus no enred.link self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen', 'enred.network': ND_DHCP4 % 'enred', 'enblue.network': ND_DHCP4 % 'enblue'}) def test_def_in_lib(self): libdir = os.path.join(self.workdir.name, 'lib', 'netplan') rundir = os.path.join(self.workdir.name, 'run', 'netplan') os.makedirs(libdir) os.makedirs(rundir) # b.yaml is in /etc/netplan too which should have precedence with open(os.path.join(libdir, 'b.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: {notme: {dhcp4: true}}''') # /run should trump /lib too with open(os.path.join(libdir, 'c.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: {alsonot: {dhcp4: true}}''') with open(os.path.join(rundir, 'c.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: {enyellow: {dhcp4: true}}''') # this should be considered with open(os.path.join(libdir, 'd.yaml'), 'w') as f: f.write('''network: version: 2 ethernets: {enblue: {dhcp4: true}}''') self.generate('''network: version: 2 ethernets: engreen: {dhcp4: true}''', confs={'b': '''network: version: 2 ethernets: {enred: {wakeonlan: true}}'''}) self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen', 'enred.link': '[Match]\nOriginalName=enred\n\n[Link]\nWakeOnLan=magic\n', 'enred.network': '''[Match] Name=enred [Network] LinkLocalAddressing=ipv6 ''', 'enyellow.network': ND_DHCP4 % 'enyellow', 'enblue.network': ND_DHCP4 % 'enblue'}) def test_wifi_access_points_merging(self): self.generate('''network: version: 2 wifis: wlan0: dhcp4: true access-points: "mywifi": password: "aaaaaaaa"''', confs={'newwifi': '''network: version: 2 wifis: wlan0: dhcp4: true access-points: "mynewwifi": password: "aaaaaaaa"'''}) self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant network={ ssid="mynewwifi" key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="aaaaaaaa" } network={ ssid="mywifi" key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="aaaaaaaa" } """) def test_wifi_access_points_overwriting(self): ''' If we find an AP that is already defined we drop the first one. XXX: this test must be removed once we implement support for AP merging ''' self.generate('''network: version: 2 wifis: wlan0: dhcp4: true access-points: "mywifi": password: "aaaaaaaa"''', confs={'newwifi': '''network: version: 2 wifis: wlan0: dhcp4: true access-points: "mywifi": password: "bbbbbbbb"'''}) self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant network={ ssid="mywifi" key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="bbbbbbbb" } """) netplan-1.0/tests/generator/test_dhcp_overrides.py000066400000000000000000000252341457004145200225640ustar00rootroot00000000000000# # Tests for DHCP override handling in netplan generator # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .base import (TestBase, ND_DHCP4, ND_DHCP4_NOMTU, ND_DHCP6, ND_DHCP6_NOMTU, ND_DHCPYES, ND_DHCPYES_NOMTU) class TestNetworkd(TestBase): # Common tests for dhcp override booleans def assert_dhcp_overrides_bool(self, override_name, networkd_name): # dhcp4 yes self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: yes ''' % override_name) # silently ignored since yes is the default self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) # dhcp6 yes self.generate('''network: version: 2 ethernets: engreen: dhcp6: yes dhcp6-overrides: %s: yes ''' % override_name) # silently ignored since yes is the default self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen'}) # dhcp4 and dhcp6 both yes self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: yes dhcp6: yes dhcp6-overrides: %s: yes ''' % (override_name, override_name)) # silently ignored since yes is the default self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen'}) # dhcp4 no self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: no ''' % override_name) self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen' + '%s=false\n' % networkd_name}) # dhcp6 no self.generate('''network: version: 2 ethernets: engreen: dhcp6: yes dhcp6-overrides: %s: no ''' % override_name) self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen' + '%s=false\n' % networkd_name}) # dhcp4 and dhcp6 both no self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: no dhcp6: yes dhcp6-overrides: %s: no ''' % (override_name, override_name)) self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen' + '%s=false\n' % networkd_name}) # mismatched values err = self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: yes dhcp6: yes dhcp6-overrides: %s: no ''' % (override_name, override_name), expect_fail=True) self.assertEqual(err.strip(), 'ERROR: engreen: networkd requires that ' '%s has the same value in both dhcp4_overrides and dhcp6_overrides' % override_name) # Common tests for dhcp override strings def assert_dhcp_overrides_string(self, override_name, networkd_name): # dhcp4 only self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: foo ''' % override_name) self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen' + '%s=foo\n' % networkd_name}) # dhcp6 only self.generate('''network: version: 2 ethernets: engreen: dhcp6: yes dhcp6-overrides: %s: foo ''' % override_name) self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen' + '%s=foo\n' % networkd_name}) # dhcp4 and dhcp6 self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: foo dhcp6: yes dhcp6-overrides: %s: foo ''' % (override_name, override_name)) self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen' + '%s=foo\n' % networkd_name}) # mismatched values err = self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: foo dhcp6: yes dhcp6-overrides: %s: bar ''' % (override_name, override_name), expect_fail=True) self.assertEqual(err.strip(), 'ERROR: engreen: networkd requires that ' '%s has the same value in both dhcp4_overrides and dhcp6_overrides' % override_name) # Common tests for dhcp override booleans def assert_dhcp_mtu_overrides_bool(self, override_name, networkd_name): # dhcp4 yes self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: yes ''' % override_name) self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) # dhcp6 yes self.generate('''network: version: 2 ethernets: engreen: dhcp6: yes dhcp6-overrides: %s: yes ''' % override_name) # silently ignored since yes is the default self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen'}) # dhcp4 and dhcp6 both yes self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: yes dhcp6: yes dhcp6-overrides: %s: yes ''' % (override_name, override_name)) # silently ignored since yes is the default self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen'}) # dhcp4 no self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: no ''' % override_name) self.assert_networkd({'engreen.network': ND_DHCP4_NOMTU % 'engreen'}) # dhcp6 no self.generate('''network: version: 2 ethernets: engreen: dhcp6: yes dhcp6-overrides: %s: no ''' % override_name) self.assert_networkd({'engreen.network': ND_DHCP6_NOMTU % 'engreen'}) # dhcp4 and dhcp6 both no self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: no dhcp6: yes dhcp6-overrides: %s: no ''' % (override_name, override_name)) self.assert_networkd({'engreen.network': ND_DHCPYES_NOMTU % 'engreen'}) # mismatched values err = self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: yes dhcp6: yes dhcp6-overrides: %s: no ''' % (override_name, override_name), expect_fail=True) self.assertEqual(err.strip(), 'ERROR: engreen: networkd requires that ' '%s has the same value in both dhcp4_overrides and dhcp6_overrides' % override_name) def assert_dhcp_overrides_guint(self, override_name, networkd_name): # dhcp4 only self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: 6000 ''' % override_name) self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=6000 UseMTU=true '''}) # dhcp6 only self.generate('''network: version: 2 ethernets: engreen: dhcp6: yes dhcp6-overrides: %s: 6000 ''' % override_name) self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=ipv6 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=6000 UseMTU=true '''}) # dhcp4 and dhcp6 self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: 6000 dhcp6: yes dhcp6-overrides: %s: 6000 ''' % (override_name, override_name)) self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=yes LinkLocalAddressing=ipv6 [DHCP] RouteMetric=6000 UseMTU=true '''}) # mismatched values err = self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp4-overrides: %s: 3333 dhcp6: yes dhcp6-overrides: %s: 5555 ''' % (override_name, override_name), expect_fail=True) self.assertEqual(err.strip(), 'ERROR: engreen: networkd requires that ' '%s has the same value in both dhcp4_overrides and dhcp6_overrides' % override_name) def test_dhcp_overrides_use_dns(self): self.assert_dhcp_overrides_bool('use-dns', 'UseDNS') def test_dhcp_overrides_use_domains(self): self.assert_dhcp_overrides_string('use-domains', 'UseDomains') def test_dhcp_overrides_use_ntp(self): self.assert_dhcp_overrides_bool('use-ntp', 'UseNTP') def test_dhcp_overrides_send_hostname(self): self.assert_dhcp_overrides_bool('send-hostname', 'SendHostname') def test_dhcp_overrides_use_hostname(self): self.assert_dhcp_overrides_bool('use-hostname', 'UseHostname') def test_dhcp_overrides_hostname(self): self.assert_dhcp_overrides_string('hostname', 'Hostname') def test_dhcp_overrides_use_mtu(self): self.assert_dhcp_mtu_overrides_bool('use-mtu', 'UseMTU') def test_dhcp_overrides_default_metric(self): self.assert_dhcp_overrides_guint('route-metric', 'RouteMetric') def test_dhcp_overrides_use_routes(self): self.assert_dhcp_overrides_bool('use-routes', 'UseRoutes') class TestNetworkManager(TestBase): def test_override_default_metric_v4(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp4: yes dhcp4-overrides: route-metric: 3333 ''') # silently ignored since yes is the default self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=auto route-metric=3333 [ipv6] method=ignore '''}) def test_override_default_metric_v6(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp4: yes dhcp6: yes dhcp6-overrides: route-metric: 6666 ''') # silently ignored since yes is the default self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=auto ip6-privacy=0 route-metric=6666 '''}) netplan-1.0/tests/generator/test_dummies.py000066400000000000000000000045711457004145200212300ustar00rootroot00000000000000# # Tests for dummy devices config generated via netplan # wokeignore:rule=dummy # # Copyright (C) 2023 Canonical, Ltd. # Author: Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .base import ND_DUMMY, ND_WITHIP, TestBase # wokeignore:rule=dummy class NetworkManager(TestBase): def test_basic(self): self.generate('''network: version: 2 renderer: NetworkManager dummy-devices: # wokeignore:rule=dummy dm0: addresses: - 192.168.1.2/24 routes: - to: 1.2.3.4 via: 192.168.1.2''') self.assert_nm({'dm0': '''[connection] id=netplan-dm0 type={} interface-name=dm0 [ipv4] method=manual address1=192.168.1.2/24 route1=1.2.3.4,192.168.1.2 [ipv6] method=ignore '''.format(('dummy'))}) # wokeignore:rule=dummy class TestNetworkd(TestBase): def test_basic(self): self.generate('''network: version: 2 dummy-devices: # wokeignore:rule=dummy dm0: addresses: - 192.168.1.2/24 routes: - to: 1.2.3.4 via: 192.168.1.2''') self.assert_networkd({'dm0.network': ND_WITHIP % ('dm0', '192.168.1.2/24') + ''' [Route] Destination=1.2.3.4 Gateway=192.168.1.2 ''', 'dm0.netdev': ND_DUMMY % ('dm0')}) # wokeignore:rule=dummy class TestNetplanYAMLv2(TestBase): '''No asserts are needed. The generate() method implicitly checks the (re-)generated YAML. ''' def test_basic(self): self.generate('''network: version: 2 dummy-devices: # wokeignore:rule=dummy dm0: {}''') def test_interface_ipv4(self): self.generate('''network: version: 2 dummy-devices: # wokeignore:rule=dummy dm0: addresses: - 192.168.1.2/24 routes: - to: 1.2.3.4 via: 192.168.1.2''') netplan-1.0/tests/generator/test_errors.py000066400000000000000000000664301457004145200211030ustar00rootroot00000000000000# # Tests for common invalid syntax/errors in config # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .base import TestBase class TestConfigErrors(TestBase): def test_malformed_yaml(self): err = self.generate('network:\n version: %&', expect_fail=True) self.assertIn('Invalid YAML', err) self.assertIn('found character that cannot start any token', err) def test_wrong_indent(self): err = self.generate('network:\n version: 2\n foo: *', expect_fail=True) self.assertIn('Invalid YAML', err) self.assertIn('inconsistent indentation', err) def test_yaml_expected_scalar(self): err = self.generate('network:\n version: {}', expect_fail=True) self.assertIn('expected scalar', err) def test_yaml_expected_sequence(self): err = self.generate('''network: version: 2 bridges: br0: interfaces: {}''', expect_fail=True) self.assertIn('expected sequence', err) def test_yaml_expected_mapping(self): err = self.generate('network:\n version', expect_fail=True) self.assertIn('expected mapping', err) def test_invalid_bool(self): err = self.generate('''network: version: 2 ethernets: id0: wakeonlan: wut ''', expect_fail=True) self.assertIn("invalid boolean value 'wut'", err) def test_invalid_version(self): err = self.generate('network:\n version: 1', expect_fail=True) self.assertIn('Only version 2 is supported', err) def test_id_redef_type_mismatch(self): err = self.generate('''network: version: 2 ethernets: id0: wakeonlan: true''', confs={'redef': '''network: version: 2 bridges: id0: wakeonlan: true'''}, expect_fail=True) self.assertIn("Updated definition 'id0' changes device type", err) def test_set_name_without_match(self): err = self.generate('''network: version: 2 ethernets: def1: set-name: lom1 ''', expect_fail=True) self.assertIn("def1: 'set-name:' requires 'match:' properties", err) def test_virtual_set_name(self): err = self.generate('''network: version: 2 bridges: br0: set_name: br1''', expect_fail=True) self.assertIn("unknown key 'set_name'", err) def test_virtual_match(self): err = self.generate('''network: version: 2 bridges: br0: match: driver: foo''', expect_fail=True) self.assertIn("unknown key 'match'", err) def test_virtual_wol(self): err = self.generate('''network: version: 2 bridges: br0: wakeonlan: true''', expect_fail=True) self.assertIn("unknown key 'wakeonlan'", err) def test_unknown_global_renderer(self): err = self.generate('''network: version: 2 renderer: bogus ''', expect_fail=True) self.assertIn("unknown renderer 'bogus'", err) def test_unknown_type_renderer(self): err = self.generate('''network: version: 2 ethernets: renderer: bogus ''', expect_fail=True) self.assertIn("unknown renderer 'bogus'", err) def test_invalid_id(self): err = self.generate('''network: version: 2 ethernets: "eth 0": dhcp4: true''', expect_fail=True) self.assertIn("Invalid name 'eth 0'", err) def test_invalid_name_match(self): err = self.generate('''network: version: 2 ethernets: def1: match: name: | fo o bar dhcp4: true''', expect_fail=True) self.assertIn("Invalid name 'fo o\nbar\n'", err) def test_invalid_mac_match(self): err = self.generate('''network: version: 2 ethernets: def1: match: macaddress: 00:11:ZZ dhcp4: true''', expect_fail=True) self.assertIn("Invalid MAC address '00:11:ZZ', must be XX:XX:XX:XX:XX:XX " "or XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX", err) def test_invalid_ipoib_mode(self): err = self.generate('''network: version: 2 ethernets: ib0: dhcp4: true infiniband-mode: invalid''', expect_fail=True) self.assertIn("Value of 'infiniband-mode' needs to be 'datagram' or 'connected'", err) def test_glob_in_id(self): err = self.generate('''network: version: 2 ethernets: en*: dhcp4: true''', expect_fail=True) self.assertIn("Definition ID 'en*' must not use globbing", err) def test_wifi_duplicate_ssid(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: workplace: password: "s3kr1t" workplace: password: "c0mpany" dhcp4: yes''', expect_fail=True) self.assertIn("wl0: Duplicate access point SSID 'workplace'", err) def test_wifi_no_ap(self): err = self.generate('''network: version: 2 wifis: wl0: dhcp4: yes''', expect_fail=True) self.assertIn('wl0: No access points defined', err) def test_wifi_empty_ap(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: {} dhcp4: yes''', expect_fail=True) self.assertIn('wl0: No access points defined', err) def test_wifi_ap_unknown_key(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: workplace: something: false dhcp4: yes''', expect_fail=True) self.assertIn("unknown key 'something'", err) def test_wifi_ap_unknown_mode(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: workplace: mode: bogus''', expect_fail=True) self.assertIn("unknown wifi mode 'bogus'", err) def test_wifi_ap_unknown_band(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: workplace: band: bogus''', expect_fail=True) self.assertIn("unknown wifi band 'bogus'", err) def test_wifi_ap_invalid_freq24(self): err = self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: workplace: band: 2.4GHz channel: 15''', expect_fail=True) self.assertIn("ERROR: invalid 2.4GHz WiFi channel: 15", err) def test_wifi_ap_invalid_freq5(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: workplace: band: 5GHz channel: 14''', expect_fail=True) self.assertIn("ERROR: invalid 5GHz WiFi channel: 14", err) def test_wifi_invalid_hidden(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: hidden: hidden: maybe''', expect_fail=True) self.assertIn("invalid boolean value 'maybe'", err) def test_invalid_ipv4_address(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 192.168.14/24 - 2001:FFfe::1/64''', expect_fail=True) self.assertIn("malformed address '192.168.14/24', must be X.X.X.X/NN", err) def test_missing_ipv4_prefixlen(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 192.168.14.1''', expect_fail=True) self.assertIn("address '192.168.14.1' is missing /prefixlength", err) def test_empty_ipv4_prefixlen(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 192.168.14.1/''', expect_fail=True) self.assertIn("invalid prefix length in address '192.168.14.1/'", err) def test_invalid_ipv4_prefixlen(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 192.168.14.1/33''', expect_fail=True) self.assertIn("invalid prefix length in address '192.168.14.1/33'", err) def test_invalid_ipv6_address(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 2001:G::1/64''', expect_fail=True) self.assertIn("malformed address '2001:G::1/64', must be X.X.X.X/NN", err) def test_missing_ipv6_prefixlen(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 2001::1''', expect_fail=True) self.assertIn("address '2001::1' is missing /prefixlength", err) def test_invalid_ipv6_prefixlen(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 2001::1/129''', expect_fail=True) self.assertIn("invalid prefix length in address '2001::1/129'", err) def test_empty_ipv6_prefixlen(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 2001::1/''', expect_fail=True) self.assertIn("invalid prefix length in address '2001::1/'", err) def test_invalid_addr_gen_mode(self): err = self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: ipv6-address-generation: 0''', expect_fail=True) self.assertIn("unknown ipv6-address-generation '0'", err) def test_addr_gen_mode_not_supported(self): err = self.generate('''network: version: 2 ethernets: engreen: ipv6-address-generation: stable-privacy''', expect_fail=True) self.assertIn("ERROR: engreen: ipv6-address-generation mode is not supported by networkd", err) def test_addr_gen_mode_and_addr_gen_token(self): err = self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: ipv6-address-token: "::2" ipv6-address-generation: eui64''', expect_fail=True) self.assertIn("engreen: ipv6-address-generation and ipv6-address-token are mutually exclusive", err) def test_invalid_addr_gen_token(self): err = self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: ipv6-address-token: INVALID''', expect_fail=True) self.assertIn("invalid ipv6-address-token 'INVALID'", err) def test_nm_devices_missing_passthrough(self): err = self.generate('''network: version: 2 renderer: NetworkManager nm-devices: engreen: networkmanager: passthrough: connection.uuid: "123456"''', expect_fail=True) self.assertIn("engreen: network type 'nm-devices:' needs to provide a 'connection.type' via passthrough", err) def test_invalid_address_node_type(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: [[192.168.1.15]]''', expect_fail=True) self.assertIn("expected either scalar or mapping (check indentation)", err) def test_invalid_address_option_value(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 0.0.0.0.0/24: lifetime: 0''', expect_fail=True) self.assertIn("malformed address '0.0.0.0.0/24', must be X.X.X.X/NN or X:X:X:X:X:X:X:X/NN", err) def test_invalid_address_option_lifetime(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: - 192.168.1.15/24: lifetime: 1''', expect_fail=True) self.assertIn("invalid lifetime value '1'", err) def test_invalid_nm_options(self): err = self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: - 192.168.1.15/24: lifetime: 0''', expect_fail=True) self.assertIn('NetworkManager does not support address options', err) def test_invalid_gateway4(self): for a in ['300.400.1.1', '1.2.3', '192.168.14.1/24']: err = self.generate('''network: version: 2 ethernets: engreen: gateway4: %s''' % a, expect_fail=True) self.assertIn("invalid IPv4 address '%s'" % a, err) def test_invalid_gateway6(self): for a in ['1234', '1:::c', '1234::1/50']: err = self.generate('''network: version: 2 ethernets: engreen: gateway6: %s''' % a, expect_fail=True) self.assertIn("invalid IPv6 address '%s'" % a, err) def test_multiple_ip4_gateways(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: [192.168.22.78/24] gateway4: 192.168.22.1 enblue: addresses: [10.49.34.4/16] gateway4: 10.49.2.38''', expect_fail=False) self.assertIn("Problem encountered while validating default route consistency", err) self.assertIn("Conflicting default route declarations for IPv4 (table: main, metric: default)", err) self.assertIn("engreen", err) self.assertIn("enblue", err) def test_multiple_ip6_gateways(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: [2001:FFfe::1/62] gateway6: 2001:FFfe::2 enblue: addresses: [2001:FFfe::33/62] gateway6: 2001:FFfe::34''', expect_fail=False) self.assertIn("Problem encountered while validating default route consistency", err) self.assertIn("Conflicting default route declarations for IPv6 (table: main, metric: default)", err) self.assertIn("engreen", err) self.assertIn("enblue", err) def test_gateway_and_default_route(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: [10.49.34.4/16] gateway4: 10.49.2.38 routes: - to: default via: 10.49.65.89''', expect_fail=False) self.assertIn("Problem encountered while validating default route consistency", err) self.assertIn("Conflicting default route declarations for IPv4 (table: main, metric: default)", err) self.assertIn("engreen", err) def test_multiple_default_routes_on_other_table(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: [10.49.34.4/16] routes: - to: default via: 10.49.65.89 enblue: addresses: [10.50.35.3/16] routes: - to: default via: 10.49.65.89 table: 23 enred: addresses: [172.137.1.4/24] routes: - to: default via: 172.137.1.1 table: 23 ''', expect_fail=False) self.assertIn("Problem encountered while validating default route consistency", err) self.assertIn("Conflicting default route declarations for IPv4 (table: 23, metric: default)", err) self.assertIn("enblue", err) self.assertIn("enred", err) self.assertNotIn("engreen", err) def test_multiple_default_routes_on_specific_metrics(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: [10.49.34.4/16] routes: - to: default via: 10.49.65.89 metric: 100 enblue: addresses: [10.50.35.3/16] routes: - to: default via: 10.49.65.89 metric: 600 enred: addresses: [172.137.1.4/24] routes: - to: default via: 172.137.1.1 metric: 600 ''', expect_fail=False) self.assertIn("Problem encountered while validating default route consistency", err) self.assertIn("Conflicting default route declarations for IPv4 (table: main, metric: 600)", err) self.assertIn("enblue", err) self.assertIn("enred", err) self.assertNotIn("engreen", err) def test_default_route_and_0(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: [10.49.34.4/16] routes: - to: default via: 10.49.65.89 - to: 0.0.0.0/0 via: 10.49.65.67''', expect_fail=False) self.assertIn("Problem encountered while validating default route consistency", err) self.assertIn("Conflicting default route declarations for IPv4 (table: main, metric: default)", err) self.assertIn("engreen", err) def test_invalid_nameserver_ipv4(self): for a in ['300.400.1.1', '1.2.3', '192.168.14.1/24']: err = self.generate('''network: version: 2 ethernets: engreen: nameservers: addresses: [%s]''' % a, expect_fail=True) self.assertIn("malformed address '%s'" % a, err) def test_invalid_nameserver_ipv6(self): for a in ['1234', '1:::c', '1234::1/50']: err = self.generate('''network: version: 2 ethernets: engreen: nameservers: addresses: ["%s"]''' % a, expect_fail=True) self.assertIn("malformed address '%s'" % a, err) def test_vlan_missing_id(self): err = self.generate('''network: version: 2 ethernets: {en1: {}} vlans: ena: {link: en1}''', expect_fail=True) self.assertIn("missing 'id' property", err) def test_vlan_invalid_id(self): err = self.generate('''network: version: 2 ethernets: {en1: {}} vlans: ena: {id: a, link: en1}''', expect_fail=True) self.assertIn("invalid unsigned int value 'a'", err) err = self.generate('''network: version: 2 ethernets: {en1: {}} vlans: ena: {id: 4095, link: en1}''', expect_fail=True) self.assertIn("invalid id '4095'", err) def test_vlan_missing_link(self): err = self.generate('''network: version: 2 vlans: ena: {id: 1}''', expect_fail=True) self.assertIn("ena: missing 'link' property", err) def test_vlan_unknown_link(self): err = self.generate('''network: version: 2 vlans: ena: {id: 1, link: en1}''', expect_fail=True) self.assertIn("ena: interface 'en1' is not defined", err) def test_vlan_unknown_renderer(self): err = self.generate('''network: version: 2 ethernets: {en1: {}} vlans: ena: {id: 1, link: en1, renderer: foo}''', expect_fail=True) self.assertIn("unknown renderer 'foo'", err) def test_device_bad_route_to(self): self.generate('''network: version: 2 ethernets: engreen: routes: - to: badlocation via: 192.168.14.20 metric: 100 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_bad_route_via(self): self.generate('''network: version: 2 ethernets: engreen: routes: - to: 10.10.0.0/16 via: badgateway metric: 100 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_bad_route_metric(self): self.generate('''network: version: 2 ethernets: engreen: routes: - to: 10.10.0.0/16 via: 10.1.1.1 metric: -1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_bad_route_mtu(self): err = self.generate('''network: version: 2 ethernets: engreen: routes: - to: 10.10.0.0/16 via: 10.1.1.1 mtu: -1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) self.assertIn("invalid unsigned int value '-1'", err) def test_device_bad_route_congestion_window(self): err = self.generate('''network: version: 2 ethernets: engreen: routes: - to: 10.10.0.0/16 via: 10.1.1.1 congestion-window: -1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) self.assertIn("invalid unsigned int value '-1'", err) def test_device_bad_route_advertised_receive_window(self): err = self.generate('''network: version: 2 ethernets: engreen: routes: - to: 10.10.0.0/16 via: 10.1.1.1 advertised-receive-window: -1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) self.assertIn("invalid unsigned int value '-1'", err) def test_device_route_family_mismatch_ipv6_to(self): self.generate('''network: version: 2 ethernets: engreen: routes: - to: 2001:dead:beef::0/16 via: 10.1.1.1 metric: 1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_route_family_mismatch_ipv4_to(self): self.generate('''network: version: 2 ethernets: engreen: routes: - via: 2001:dead:beef::2 to: 10.10.10.0/24 metric: 1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_route_missing_to(self): self.generate('''network: version: 2 ethernets: engreen: routes: - via: 2001:dead:beef::2 metric: 1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_route_missing_via(self): self.generate('''network: version: 2 ethernets: engreen: routes: - to: 2001:dead:beef::2 scope: global metric: 1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_route_type_missing_to(self): self.generate('''network: version: 2 ethernets: engreen: routes: - via: 2001:dead:beef::2 type: prohibit metric: 1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_route_scope_link_missing_to(self): self.generate('''network: version: 2 ethernets: engreen: routes: - via: 2001:dead:beef::2 scope: link metric: 1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_route_invalid_onlink(self): self.generate('''network: version: 2 ethernets: engreen: routes: - via: 2001:dead:beef::2 to: 2000:cafe:cafe::1/24 on-link: 1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_route_invalid_table(self): self.generate('''network: version: 2 ethernets: engreen: routes: - via: 2001:dead:beef::2 to: 2000:cafe:cafe::1/24 table: -1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_route_invalid_type(self): self.generate('''network: version: 2 ethernets: engreen: routes: - via: 2001:dead:beef::2 to: 2000:cafe:cafe::1/24 type: thisisinvalidtype addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_route_invalid_scope(self): self.generate('''network: version: 2 ethernets: engreen: routes: - via: 2001:dead:beef::2 to: 2000:cafe:cafe::1/24 scope: linky addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_ip_rule_mismatched_addresses(self): self.generate('''network: version: 2 ethernets: engreen: routing-policy: - from: 10.10.10.0/24 to: 2000:dead:beef::3/64 table: 50 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_ip_rule_missing_address(self): self.generate('''network: version: 2 ethernets: engreen: routing-policy: - table: 50 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_ip_rule_invalid_tos(self): self.generate('''network: version: 2 ethernets: engreen: routing-policy: - from: 10.10.10.0/24 type-of-service: 256 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_ip_rule_invalid_prio(self): self.generate('''network: version: 2 ethernets: engreen: routing-policy: - from: 10.10.10.0/24 priority: -1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_ip_rule_invalid_table(self): self.generate('''network: version: 2 ethernets: engreen: routing-policy: - from: 10.10.10.0/24 table: -1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_ip_rule_invalid_fwmark(self): self.generate('''network: version: 2 ethernets: engreen: routing-policy: - from: 10.10.10.0/24 mark: -1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_device_ip_rule_invalid_address(self): self.generate('''network: version: 2 ethernets: engreen: routing-policy: - to: 10.10.10.0/24 from: someinvalidaddress mark: 1 addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) def test_invalid_dhcp_identifier(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp-identifier: invalid''', expect_fail=True) def test_invalid_accept_ra(self): err = self.generate('''network: version: 2 bridges: br0: accept-ra: j''', expect_fail=True) self.assertIn('invalid boolean', err) def test_invalid_link_local_set(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp6: yes link-local: invalid''', expect_fail=True) def test_invalid_link_local_value(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: yes dhcp6: yes link-local: [ invalid, ]''', expect_fail=True) def test_invalid_yaml_tabs(self): err = self.generate('''\t''', expect_fail=True) self.assertIn("tabs are not allowed for indent", err) def test_invalid_yaml_undefined_alias(self): err = self.generate('''network: version: 2 ethernets: *engreen: dhcp4: yes''', expect_fail=True) self.assertIn("aliases are not supported", err) def test_invalid_yaml_undefined_alias_at_eof(self): err = self.generate('''network: version: 2 ethernets: engreen: dhcp4: *yes''', expect_fail=True) self.assertIn("aliases are not supported", err) def test_invalid_activation_mode(self): err = self.generate('''network: version: 2 ethernets: engreen: activation-mode: invalid''', expect_fail=True) self.assertIn("needs to be 'manual' or 'off'", err) def test_nm_only_supports_unicast_routes(self): err = self.generate('''network: version: 2 renderer: NetworkManager vrfs: vrf100: table: 100 routes: - to: 1.2.3.4/24 type: throw routing-policy: - to: 1.2.3.4/24''', expect_fail=True) self.assertIn("NetworkManager only supports unicast routes", err) netplan-1.0/tests/generator/test_ethernets.py000066400000000000000000000512001457004145200215550ustar00rootroot00000000000000# # Tests for ethernet devices config generated via netplan # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os from .base import TestBase, ND_DHCP4, UDEV_MAC_RULE, UDEV_NO_MAC_RULE, \ NM_MANAGED, NM_UNMANAGED, NM_MANAGED_MAC, NM_UNMANAGED_MAC, \ NM_MANAGED_DRIVER, NM_UNMANAGED_DRIVER class TestNetworkd(TestBase): def test_eth_wol(self): self.generate('''network: version: 2 ethernets: eth0: wakeonlan: true dhcp4: n''') self.assert_networkd({'eth0.link': '[Match]\nOriginalName=eth0\n\n[Link]\nWakeOnLan=magic\n', 'eth0.network': '''[Match] Name=eth0 [Network] LinkLocalAddressing=ipv6 '''}) self.assert_networkd_udev(None) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'eth0') # should not allow NM to manage everything self.assertFalse(os.path.exists(self.nm_enable_all_conf)) def test_eth_lldp(self): self.generate('''network: version: 2 ethernets: eth0: dhcp4: n emit-lldp: true''') self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Network] EmitLLDP=true LinkLocalAddressing=ipv6 '''}) def test_eth_mtu(self): self.generate('''network: version: 2 ethernets: eth1: mtu: 1280 dhcp4: n''') self.assert_networkd({'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=1280\n', 'eth1.network': '''[Match] Name=eth1 [Link] MTUBytes=1280 [Network] LinkLocalAddressing=ipv6 '''}) self.assert_networkd_udev(None) def test_eth_match_by_driver_rename(self): self.generate('''network: version: 2 ethernets: def1: match: driver: ixgbe set-name: lom1''') self.assert_networkd({'def1.link': '[Match]\nDriver=ixgbe\n\n[Link]\nName=lom1\nWakeOnLan=off\n', 'def1.network': '''[Match] Driver=ixgbe Name=lom1 [Network] LinkLocalAddressing=ipv6 '''}) self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('ixgbe', 'lom1'))}) # NM cannot match by driver, so denylisting needs to happen via udev self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'lom1' + NM_UNMANAGED_DRIVER % 'ixgbe') def test_eth_match_by_mac_rename(self): self.generate('''network: version: 2 ethernets: def1: match: macaddress: 11:22:33:44:55:66 set-name: lom1''') self.assert_networkd({'def1.link': '[Match]\nPermanentMACAddress=11:22:33:44:55:66\n\n[Link]\nName=lom1\nWakeOnLan=off\n', 'def1.network': '''[Match] PermanentMACAddress=11:22:33:44:55:66 Name=lom1 [Network] LinkLocalAddressing=ipv6 '''}) self.assert_networkd_udev({'def1.rules': (UDEV_MAC_RULE % ('?*', '11:22:33:44:55:66', 'lom1'))}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'lom1' + NM_UNMANAGED_MAC % '11:22:33:44:55:66') # https://bugs.launchpad.net/netplan/+bug/1848474 def test_eth_match_by_mac_infiniband(self): self.generate('''network: version: 2 ethernets: ib0: match: macaddress: 11:22:33:44:55:66:77:88:99:00:11:22:33:44:55:66:77:88:99:00 dhcp4: true infiniband-mode: connected''') self.assert_networkd({'ib0.network': '''[Match] PermanentMACAddress=11:22:33:44:55:66:77:88:99:00:11:22:33:44:55:66:77:88:99:00 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true [IPoIB] Mode=connected '''}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED_MAC % '11:22:33:44:55:66:77:88:99:00:11:22:33:44:55:66:77:88:99:00') def test_eth_implicit_name_match_dhcp4(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: y''') self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) self.assert_networkd_udev(None) def test_eth_match_dhcp4(self): self.generate('''network: version: 2 ethernets: def1: match: driver: ixgbe dhcp4: true''') self.assert_networkd({'def1.network': '''[Match] Driver=ixgbe [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) self.assert_networkd_udev(None) self.assert_nm_udev(NM_UNMANAGED_DRIVER % 'ixgbe') def test_eth_match_name(self): self.generate('''network: version: 2 ethernets: def1: match: name: green dhcp4: true''') self.assert_networkd({'def1.network': ND_DHCP4 % 'green'}) self.assert_networkd_udev(None) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'green') def test_eth_set_mac(self): self.generate('''network: version: 2 ethernets: def1: match: name: green macaddress: 00:01:02:03:04:05 dhcp4: true''') self.assert_networkd({'def1.network': (ND_DHCP4 % 'green') .replace('[Network]', '[Link]\nMACAddress=00:01:02:03:04:05\n\n[Network]') }) self.assert_networkd_udev(None) def test_eth_set_mac_value_not_valid(self): res = self.generate('''network: version: 2 ethernets: def1: match: name: green macaddress: typorandom dhcp4: true''', expect_fail=True) self.assertIn('Invalid MAC address \'typorandom\'', res) def test_eth_set_mac_value_only_valid_for_network_manager(self): res = self.generate('''network: version: 2 ethernets: def1: match: name: green macaddress: stable dhcp4: true''', expect_fail=True) self.assertIn('networkd backend does not support the MAC address option \'stable\'', res) def test_eth_set_mac_special_values(self): self.generate('''network: version: 2 ethernets: eth0: macaddress: permanent dhcp4: true eth1: macaddress: random dhcp4: true''') self.assert_networkd({'eth0.link': '[Match]\nOriginalName=eth0\n\n[Link]\nWakeOnLan=off\nMACAddressPolicy=persistent\n', 'eth0.network': ND_DHCP4 % 'eth0', 'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMACAddressPolicy=random\n', 'eth1.network': ND_DHCP4 % 'eth1'}) def test_eth_match_name_rename(self): self.generate('''network: version: 2 ethernets: def1: match: name: green set-name: blue dhcp4: true''') # the .network needs to match on the renamed name self.assert_networkd({'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nName=blue\nWakeOnLan=off\n', 'def1.network': ND_DHCP4 % 'blue'}) # The udev rules engine does support renaming by name self.assert_networkd_udev(None) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'blue' + NM_UNMANAGED % 'green') def test_eth_match_all_names(self): self.generate('''network: version: 2 ethernets: def1: match: {name: "*"} dhcp4: true''') self.assert_networkd({'def1.network': ND_DHCP4 % '*'}) self.assert_networkd_udev(None) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % '*') def test_eth_match_all(self): self.generate('''network: version: 2 ethernets: def1: match: {} dhcp4: true''') self.assert_networkd({'def1.network': '[Match]\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n' '[DHCP]\nRouteMetric=100\nUseMTU=true\n'}) self.assert_networkd_udev(None) self.assert_nm(None, '''[device-netplan.ethernets.def1] match-device=type:ethernet managed=0\n\n''') self.assert_nm_udev(None) def test_match_multiple(self): self.generate('''network: version: 2 ethernets: def1: match: name: en1s* macaddress: 00:11:22:33:44:55 dhcp4: on''') self.assert_networkd({'def1.network': '''[Match] PermanentMACAddress=00:11:22:33:44:55 Name=en1s* [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) self.assert_nm(None) self.assert_nm_udev('SUBSYSTEM=="net", ACTION=="add|change|move", ENV{ID_NET_NAME}=="en1s*", ' 'ATTR{address}=="00:11:22:33:44:55", ENV{NM_UNMANAGED}="1"\n') class TestNetworkManager(TestBase): def test_eth_wol(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: wakeonlan: true''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=1 [ipv4] method=link-local [ipv6] method=ignore '''}) # should allow NM to manage everything else self.assertTrue(os.path.exists(self.nm_enable_all_conf)) self.assert_networkd({'eth0.link': '[Match]\nOriginalName=eth0\n\n[Link]\nWakeOnLan=magic\n'}) self.assert_nm_udev(NM_MANAGED % 'eth0') def test_eth_mtu(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth1: mtu: 1280 dhcp4: n''') self.assert_networkd({'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=1280\n'}) self.assert_nm({'eth1': '''[connection] id=netplan-eth1 type=ethernet interface-name=eth1 [ethernet] wake-on-lan=0 mtu=1280 [ipv4] method=link-local [ipv6] method=ignore '''}) def test_eth_set_mac(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: macaddress: 00:01:02:03:04:05 dhcp4: true''') self.assert_networkd(None) self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 cloned-mac-address=00:01:02:03:04:05 [ipv4] method=auto [ipv6] method=ignore '''}) def test_eth_set_mac_special_values(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: macaddress: preserve dhcp4: true eth1: macaddress: permanent dhcp4: true eth2: macaddress: random dhcp4: true eth3: macaddress: stable dhcp4: true''') self.assert_networkd(None) self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 cloned-mac-address=preserve [ipv4] method=auto [ipv6] method=ignore ''', 'eth1': '''[connection] id=netplan-eth1 type=ethernet interface-name=eth1 [ethernet] wake-on-lan=0 cloned-mac-address=permanent [ipv4] method=auto [ipv6] method=ignore ''', 'eth2': '''[connection] id=netplan-eth2 type=ethernet interface-name=eth2 [ethernet] wake-on-lan=0 cloned-mac-address=random [ipv4] method=auto [ipv6] method=ignore ''', 'eth3': '''[connection] id=netplan-eth3 type=ethernet interface-name=eth3 [ethernet] wake-on-lan=0 cloned-mac-address=stable [ipv4] method=auto [ipv6] method=ignore '''}) def test_eth_set_mac_special_values_error(self): res = self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: macaddress: preservetypo dhcp4: true''', expect_fail=True) error = ("Invalid MAC address 'preservetypo', must be XX:XX:XX:XX:XX:XX, " "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX or " "one of 'permanent', 'random', 'stable', 'preserve'") self.assertIn(error, res) def test_eth_match_by_driver(self): err = self.generate('''network: version: 2 renderer: NetworkManager ethernets: def1: match: driver: ixgbe''', expect_fail=True) self.assertIn('NetworkManager definitions do not support matching by driver', err) def test_eth_match_by_drivers(self): self.generate('''network: version: 2 renderer: networkd ethernets: def1: match: driver: ["bcmgenet", "smsc*"]''') self.assert_networkd({'def1.network': '''[Match] Driver=bcmgenet smsc* [Network] LinkLocalAddressing=ipv6 '''}) def test_eth_match_by_drivers_whitespace(self): err = self.generate('''network: version: 2 ethernets: def1: match: driver: "bcmgenet smsc*"''', expect_fail=True) self.assertIn('A \'driver\' glob cannot contain whitespace', err) def test_eth_match_by_drivers_whitespace_sequence(self): err = self.generate('''network: version: 2 ethernets: def1: match: driver: ["ixgbe", "bcmgenet smsc*"]''', expect_fail=True) self.assertIn('A \'driver\' glob cannot contain whitespace', err) def test_eth_match_by_drivers_invalid_sequence(self): err = self.generate('''network: version: 2 ethernets: def1: match: driver: []''', expect_fail=True) self.assertIn('invalid sequence for \'driver\'', err) def test_eth_match_by_drivers_invalid_type(self): err = self.generate('''network: version: 2 ethernets: def1: match: driver: some_mapping: true''', expect_fail=True) self.assertIn('invalid type for \'driver\': must be a scalar or a sequence of scalars', err) def test_eth_match_by_driver_rename(self): # in this case udev will rename the device so that NM can use the name self.generate('''network: version: 2 renderer: NetworkManager ethernets: def1: match: driver: ixgbe set-name: lom1''') self.assert_networkd({'def1.link': '[Match]\nDriver=ixgbe\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('ixgbe', 'lom1'))}) self.assert_nm({'def1': '''[connection] id=netplan-def1 type=ethernet interface-name=lom1 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_nm_udev(NM_MANAGED % 'lom1' + NM_MANAGED_DRIVER % 'ixgbe') def test_eth_match_by_mac_rename(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: def1: match: macaddress: 11:22:33:44:55:66 set-name: lom1''') self.assert_networkd({'def1.link': '''[Match] PermanentMACAddress=11:22:33:44:55:66\n\n[Link]\nName=lom1\nWakeOnLan=off\n'''}) self.assert_networkd_udev({'def1.rules': (UDEV_MAC_RULE % ('?*', '11:22:33:44:55:66', 'lom1'))}) self.assert_nm({'def1': '''[connection] id=netplan-def1 type=ethernet interface-name=lom1 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_nm_udev(NM_MANAGED % 'lom1' + NM_MANAGED_MAC % '11:22:33:44:55:66') def test_eth_implicit_name_match_dhcp4(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp4: true''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) def test_eth_match_mac_dhcp4(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: def1: match: macaddress: 11:22:33:44:55:66 dhcp4: true''') self.assert_nm({'def1': '''[connection] id=netplan-def1 type=ethernet [ethernet] wake-on-lan=0 mac-address=11:22:33:44:55:66 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) def test_eth_match_name(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: def1: match: name: green dhcp4: true''') self.assert_nm({'def1': '''[connection] id=netplan-def1 type=ethernet interface-name=green [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'green') def test_eth_match_name_rename(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: def1: match: name: green set-name: blue dhcp4: true''') # The udev rules engine does support renaming by name self.assert_networkd_udev(None) # NM needs to match on the renamed name self.assert_nm({'def1': '''[connection] id=netplan-def1 type=ethernet interface-name=blue [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=ignore '''}) # ... while udev renames it self.assert_networkd({'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nName=blue\nWakeOnLan=off\n'}) self.assert_nm_udev(NM_MANAGED % 'blue' + NM_MANAGED % 'green') def test_eth_match_name_glob(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: def1: match: {name: "en*"} dhcp4: true''') self.assert_nm({'def1': '''[connection] id=netplan-def1 type=ethernet [ethernet] wake-on-lan=0 [match] interface-name=en*; [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) def test_eth_match_all(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: def1: match: {} dhcp4: true''') self.assert_nm({'def1': '''[connection] id=netplan-def1 type=ethernet [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=ignore '''}, '''[device-netplan.ethernets.def1] match-device=type:ethernet managed=1\n\n''') self.assert_nm_udev(None) self.assert_networkd({}) def test_match_multiple(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: def1: match: name: engreen macaddress: 00:11:22:33:44:55 dhcp4: yes''') self.assert_nm({'def1': '''[connection] id=netplan-def1 type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 mac-address=00:11:22:33:44:55 [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev('SUBSYSTEM=="net", ACTION=="add|change|move", ENV{ID_NET_NAME}=="engreen", ' 'ATTR{address}=="00:11:22:33:44:55", ENV{NM_UNMANAGED}="0"\n') def test_offload(self): self.generate('''network: version: 2 ethernets: eth1: receive-checksum-offload: true transmit-checksum-offload: off tcp-segmentation-offload: true tcp6-segmentation-offload: false generic-segmentation-offload: true generic-receive-offload: no large-receive-offload: true''') self.assert_networkd({'eth1.link': '''[Match] OriginalName=eth1 [Link] WakeOnLan=off ReceiveChecksumOffload=true TransmitChecksumOffload=false TCPSegmentationOffload=true TCP6SegmentationOffload=false GenericSegmentationOffload=true GenericReceiveOffload=false LargeReceiveOffload=true ''', 'eth1.network': '''[Match] Name=eth1 [Network] LinkLocalAddressing=ipv6 '''}) self.assert_networkd_udev(None) def test_offload_invalid(self): err = self.generate('''network: version: 2 ethernets: eth1: generic-receive-offload: n receive-checksum-offload: true tcp-segmentation-offload: true tcp6-segmentation-offload: false generic-segmentation-offload: true transmit-checksum-offload: xx large-receive-offload: true''', expect_fail=True) self.assertIn('invalid boolean value \'xx\'', err) # https://bugs.launchpad.net/netplan/+bug/1848474 def test_eth_match_by_mac_infiniband(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: ib0: match: macaddress: 11:22:33:44:55:66:77:88:99:00:11:22:33:44:55:66:77:88:99:00 dhcp4: true infiniband-mode: datagram''') self.assert_networkd(None) self.assert_nm({'ib0': '''[connection] id=netplan-ib0 type=infiniband [infiniband] mac-address=11:22:33:44:55:66:77:88:99:00:11:22:33:44:55:66:77:88:99:00 transport-mode=datagram [ipv4] method=auto [ipv6] method=ignore '''}) self.assert_nm_udev(NM_MANAGED_MAC % '11:22:33:44:55:66:77:88:99:00:11:22:33:44:55:66:77:88:99:00') netplan-1.0/tests/generator/test_modems.py000066400000000000000000000221411457004145200210420ustar00rootroot00000000000000# # Tests for gsm/cdma modem devices config generated via netplan # # Copyright (C) 2020 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .base import TestBase, NM_MANAGED class TestNetworkd(TestBase): '''networkd output''' def test_not_supported(self): # does not produce any output, but fails with: # "networkd backend does not support GSM modem configuration" err = self.generate('''network: version: 2 modems: mobilephone: auto-config: true''', expect_fail=True) self.assertIn("ERROR: mobilephone: networkd backend does not support GSM/CDMA modem configuration", err) self.assert_networkd({}) self.assert_nm({}) class TestNetworkManager(TestBase): '''networkmanager output''' def test_cdma_config(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: mtu: 1542 number: "#666" username: test-user password: s0s3kr1t''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=cdma interface-name=mobilephone [cdma] password=s0s3kr1t username=test-user mtu=1542 number=#666 [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_auto_config(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: auto-config: true''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm interface-name=mobilephone [gsm] auto-config=true [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_auto_config_implicit(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: number: "*99#" mtu: 1600 pin: "1234"''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm interface-name=mobilephone [gsm] auto-config=true mtu=1600 number=*99# pin=1234 [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_apn(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: apn: internet''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm interface-name=mobilephone [gsm] apn=internet [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_apn_username_password(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: apn: internet username: some-user password: some-pass''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm interface-name=mobilephone [gsm] apn=internet password=some-pass username=some-user [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_device_id(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: device-id: test''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm interface-name=mobilephone [gsm] auto-config=true device-id=test [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_network_id(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: network-id: test''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm interface-name=mobilephone [gsm] auto-config=true network-id=test [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_pin(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: pin: 1234''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm interface-name=mobilephone [gsm] auto-config=true pin=1234 [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_sim_id(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: sim-id: test''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm interface-name=mobilephone [gsm] auto-config=true sim-id=test [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_sim_operator_id(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: sim-operator-id: test''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm interface-name=mobilephone [gsm] auto-config=true sim-operator-id=test [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_gsm_example(self): self.generate('''network: version: 2 renderer: NetworkManager modems: cdc-wdm1: mtu: 1600 apn: ISP.CINGULAR username: ISP@CINGULARGPRS.COM password: CINGULAR1 number: "*99#" network-id: 24005 device-id: da812de91eec16620b06cd0ca5cbc7ea25245222 pin: 2345 sim-id: 89148000000060671234 sim-operator-id: 310260''') self.assert_nm({'cdc-wdm1': '''[connection] id=netplan-cdc-wdm1 type=gsm interface-name=cdc-wdm1 [gsm] apn=ISP.CINGULAR password=CINGULAR1 username=ISP@CINGULARGPRS.COM device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 mtu=1600 network-id=24005 number=*99# pin=2345 sim-id=89148000000060671234 sim-operator-id=310260 [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'cdc-wdm1') def test_modem_nm_integration(self): self.generate('''network: version: 2 renderer: NetworkManager modems: mobilephone: auto-config: true networkmanager: uuid: b22d8f0f-3f34-46bd-ac28-801fa87f1eb6''') self.assert_nm({'mobilephone': '''[connection] id=netplan-mobilephone type=gsm uuid=b22d8f0f-3f34-46bd-ac28-801fa87f1eb6 interface-name=mobilephone [gsm] auto-config=true [ipv4] method=link-local [ipv6] method=ignore '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'mobilephone') def test_modem_nm_integration_gsm_cdma(self): self.generate('''network: version: 2 modems: NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3: renderer: NetworkManager match: {} apn: internet2.voicestream.com networkmanager: uuid: a08c5805-7cf5-43f7-afb9-12cb30f6eca3 name: "T-Mobile Funkadelic 2" passthrough: connection.type: "bluetooth" gsm.apn: "internet2.voicestream.com" gsm.device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" gsm.username: "george.clinton.again" gsm.sim-operator-id: "310260" gsm.pin: "123456" gsm.sim-id: "89148000000060671234" gsm.password: "parliament2" gsm.network-id: "254098" ipv4.method: "auto" ipv6.method: "auto"''') self.assert_nm({'NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3': '''[connection] id=T-Mobile Funkadelic 2 #Netplan: passthrough override type=bluetooth uuid=a08c5805-7cf5-43f7-afb9-12cb30f6eca3 [gsm] apn=internet2.voicestream.com #Netplan: passthrough setting device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 #Netplan: passthrough setting username=george.clinton.again #Netplan: passthrough setting sim-operator-id=310260 #Netplan: passthrough setting pin=123456 #Netplan: passthrough setting sim-id=89148000000060671234 #Netplan: passthrough setting password=parliament2 #Netplan: passthrough setting network-id=254098 [ipv4] #Netplan: passthrough override method=auto [ipv6] #Netplan: passthrough override method=auto '''}, '''[device-netplan.modems.NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3] match-device=type:gsm managed=1\n\n''') self.assert_networkd({}) self.assert_nm_udev(None) netplan-1.0/tests/generator/test_ovs.py000066400000000000000000001166721457004145200204020ustar00rootroot00000000000000# # Common tests for netplan OpenVSwitch support # # Copyright (C) 2020 Canonical, Ltd. # Author: Łukasz 'sil2100' Zemczak # Lukas 'slyon' Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import unittest from netplan_cli.cli.ovs import OPENVSWITCH_OVS_VSCTL from .base import TestBase, ND_EMPTY, ND_WITHIP, ND_DHCP4, ND_DHCP6, \ OVS_PHYSICAL, OVS_VIRTUAL, \ OVS_BR_EMPTY, OVS_BR_DEFAULT, \ OVS_CLEANUP @unittest.skipIf(not os.path.exists(OPENVSWITCH_OVS_VSCTL), 'OpenVSwitch not installed') class TestOpenVSwitch(TestBase): '''OVS output''' def test_interface_external_ids_other_config(self): self.generate('''network: version: 2 bridges: # bridges first, to trigger multi-pass processing ovs0: interfaces: [eth0, eth1] openvswitch: {} ethernets: eth0: openvswitch: external-ids: iface-id: myhostname other-config: disable-in-band: true dhcp6: true eth1: dhcp4: true openvswitch: other-config: disable-in-band: false\n''') self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth0 ''' + OVS_BR_DEFAULT % {'iface': 'ovs0'}}, 'eth0.service': OVS_PHYSICAL % {'iface': 'eth0', 'extra': '''\ Requires=netplan-ovs-ovs0.service After=netplan-ovs-ovs0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/external-ids/iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Interface eth0 other-config:disable-in-band=true ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/other-config/disable-in-band=true '''}, 'eth1.service': OVS_PHYSICAL % {'iface': 'eth1', 'extra': '''\ Requires=netplan-ovs-ovs0.service After=netplan-ovs-ovs0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface eth1 other-config:disable-in-band=false ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-config/disable-in-band=false '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'ovs0.network': ND_EMPTY % ('ovs0', 'ipv6'), 'eth0.network': (ND_DHCP6 % 'eth0') .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0'), 'eth1.network': (ND_DHCP4 % 'eth1') .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0')}) def test_interface_invalid_external_ids_other_config(self): err = self.generate('''network: version: 2 ethernets: eth0: openvswitch: external-ids: iface-id: myhostname other-config: disable-in-band: true''', expect_fail=True) self.assertIn('eth0: Interface needs to be assigned to an OVS bridge/bond to carry external-ids/other-config', err) def test_global_external_ids_other_config(self): self.generate('''network: version: 2 openvswitch: external-ids: iface-id: myhostname other-config: disable-in-band: true ethernets: eth0: dhcp4: yes ''') self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/external-ids/iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set open_vswitch . other-config:disable-in-band=true ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/other-config/disable-in-band=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) def test_global_set_protocols(self): out = self.generate('''network: version: 2 openvswitch: protocols: [OpenFlow10, OpenFlow11, OpenFlow12, OpenFlow16] bridges: ovs0: openvswitch: {}''', skip_generated_yaml_validation=True) # OpenFlow16 won't be re-generated self.assertIn('Open vSwitch: Ignoring deprecated protocol: OpenFlow16', out) self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 ''' + OVS_BR_DEFAULT % {'iface': 'ovs0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 protocols=OpenFlow10,OpenFlow11,OpenFlow12 ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 external-ids:netplan/protocols=OpenFlow10,OpenFlow11,OpenFlow12 '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'ovs0.network': ND_EMPTY % ('ovs0', 'ipv6')}) def test_duplicate_map_entry(self): err = self.generate('''network: version: 2 openvswitch: external-ids: iface-id: myhostname iface-id: foobar ethernets: eth0: dhcp4: yes ''', expect_fail=True) self.assertIn("duplicate map entry 'iface-id'", err) def test_no_ovs_config(self): self.generate('''network: version: 2 ethernets: eth0: dhcp4: yes ''') self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) def test_bond_setup(self): self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bonds: bond0: interfaces: [eth1, eth2] openvswitch: external-ids: iface-id: myhostname bridges: br0: addresses: [192.170.1.1/24] interfaces: [bond0] openvswitch: {} ''') self.assert_ovs({'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': '''Requires=netplan-ovs-br0.service After=netplan-ovs-br0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/external-ids/iface-id=myhostname '''}, 'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), 'bond0.network': ND_EMPTY % ('bond0', 'no')}) def test_bond_no_bridge(self): err = self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bonds: bond0: interfaces: [eth1, eth2] openvswitch: {} ''', expect_fail=True) self.assertIn("Bond bond0 needs to be a member of an OpenVSwitch bridge", err) def test_bond_not_enough_interfaces(self): err = self.generate('''network: version: 2 ethernets: eth1: {} bonds: bond0: interfaces: [eth1] openvswitch: {} bridges: br0: addresses: [192.170.1.1/24] interfaces: [bond0] openvswitch: {} ''', expect_fail=True) self.assertIn("Bond bond0 needs to have at least 2 member interfaces", err) def test_bond_lacp(self): self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bonds: bond0: interfaces: [eth1, eth2] openvswitch: lacp: active bridges: br0: addresses: [192.170.1.1/24] interfaces: [bond0] openvswitch: {} ''') self.assert_ovs({'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': '''Requires=netplan-ovs-br0.service After=netplan-ovs-br0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=active ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=active '''}, 'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), 'bond0.network': ND_EMPTY % ('bond0', 'no')}) def test_bond_lacp_invalid(self): err = self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bonds: bond0: interfaces: [eth1, eth2] openvswitch: lacp: invalid bridges: br0: addresses: [192.170.1.1/24] interfaces: [bond0] openvswitch: {} ''', expect_fail=True) self.assertIn("Value of 'lacp' needs to be 'active', 'passive' or 'off", err) def test_bond_lacp_wrong_type(self): err = self.generate('''network: version: 2 ethernets: eth1: openvswitch: lacp: passive ''', expect_fail=True) self.assertIn("Key 'lacp' is only valid for interface type 'Open vSwitch bond'", err) def test_bond_mode_implicit_params(self): self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bonds: bond0: interfaces: [eth1, eth2] parameters: mode: balance-tcp # Sets OVS backend implicitly bridges: br0: addresses: [192.170.1.1/24] interfaces: [bond0] openvswitch: {} ''') self.assert_ovs({'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': '''Requires=netplan-ovs-br0.service After=netplan-ovs-br0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off ExecStart=/usr/bin/ovs-vsctl set Port bond0 bond_mode=balance-tcp ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=balance-tcp '''}, 'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), 'bond0.network': ND_EMPTY % ('bond0', 'no')}) def test_bond_mode_explicit_params(self): self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bonds: bond0: interfaces: [eth1, eth2] parameters: mode: active-backup openvswitch: {} bridges: br0: addresses: [192.170.1.1/24] interfaces: [bond0] openvswitch: {} ''') self.assert_ovs({'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': '''Requires=netplan-ovs-br0.service After=netplan-ovs-br0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off ExecStart=/usr/bin/ovs-vsctl set Port bond0 bond_mode=active-backup ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=active-backup '''}, 'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), 'bond0.network': ND_EMPTY % ('bond0', 'no')}) def test_bond_mode_ovs_invalid(self): err = self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bonds: bond0: interfaces: [eth1, eth2] parameters: mode: balance-rr openvswitch: {} bridges: br0: addresses: [192.170.1.1/24] interfaces: [bond0] openvswitch: {} ''', expect_fail=True) self.assertIn("bond0: bond mode 'balance-rr' not supported by Open vSwitch", err) def test_bridge_setup(self): self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bridges: br0: addresses: [192.170.1.1/24] interfaces: [eth1, eth2] openvswitch: {} ''') self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24')}) def test_bridge_external_ids_other_config(self): self.generate('''network: version: 2 bridges: br0: openvswitch: external-ids: iface-id: myhostname other-config: disable-in-band: true ''') self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/external-ids/iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Bridge br0 other-config:disable-in-band=true ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/other-config/disable-in-band=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the bridge has been only configured for OVS self.assert_networkd({'br0.network': ND_EMPTY % ('br0', 'ipv6')}) def test_bridge_non_default_parameters(self): self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bridges: br0: addresses: [192.170.1.1/24] interfaces: [eth1, eth2] openvswitch: fail-mode: secure mcast-snooping: true rstp: true ''') self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set-fail-mode br0 secure ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/global/set-fail-mode=secure ExecStart=/usr/bin/ovs-vsctl set Bridge br0 mcast_snooping_enable=true ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/mcast_snooping_enable=true ExecStart=/usr/bin/ovs-vsctl set Bridge br0 rstp_enable=true ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/rstp_enable=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24')}) def test_bridge_fail_mode_invalid(self): err = self.generate('''network: version: 2 bridges: br0: openvswitch: fail-mode: glorious ''', expect_fail=True) self.assertIn("Value of 'fail-mode' needs to be 'standalone' or 'secure'", err) def test_fail_mode_non_bridge(self): err = self.generate('''network: version: 2 ethernets: eth0: openvswitch: fail-mode: glorious ''', expect_fail=True) self.assertIn("Key 'fail-mode' is only valid for interface type 'Open vSwitch bridge'", err) def test_rstp_non_bridge(self): err = self.generate('''network: version: 2 ethernets: eth0: openvswitch: rstp: true ''', expect_fail=True) self.assertIn("Key is only valid for interface type 'Open vSwitch bridge'", err) def test_bridge_set_protocols(self): self.generate('''network: version: 2 bridges: br0: openvswitch: protocols: [OpenFlow10, OpenFlow11, OpenFlow15] ''') self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 protocols=OpenFlow10,OpenFlow11,OpenFlow15 ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/protocols=OpenFlow10,OpenFlow11,OpenFlow15 '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'br0.network': ND_EMPTY % ('br0', 'ipv6')}) def test_bridge_set_protocols_invalid(self): err = self.generate('''network: version: 2 bridges: br0: openvswitch: protocols: [OpenFlow10, OpenFooBar13, OpenFlow15] ''', expect_fail=True) self.assertIn("Unsupported OVS 'protocol' value: OpenFooBar13", err) def test_set_protocols_invalid_interface(self): err = self.generate('''network: version: 2 ethernets: eth0: openvswitch: protocols: [OpenFlow10, OpenFlow15] ''', expect_fail=True) self.assertIn("Key 'protocols' is only valid for interface type 'Open vSwitch bridge'", err) def test_bridge_controller(self): self.generate('''network: version: 2 bridges: br0: openvswitch: controller: addresses: ["ptcp:", "ptcp:1337", "ptcp:1337:[fe80::1234%eth0]", "pssl:1337:[fe80::1]", "ssl:10.10.10.1",\ tcp:127.0.0.1:1337, "tcp:[fe80::1234%eth0]", "tcp:[fe80::1]:1337", unix:/some/path, punix:other/path] connection-mode: out-of-band openvswitch: ssl: ca-cert: /another/path certificate: /some/path private-key: /key/path ''') self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set-controller br0 ptcp: ptcp:1337 ptcp:1337:[fe80::1234%eth0] pssl:1337:[fe80::1] ssl:10.10.10.1 \ tcp:127.0.0.1:1337 tcp:[fe80::1234%eth0] tcp:[fe80::1]:1337 unix:/some/path punix:other/path ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/global/set-controller=ptcp:,ptcp:1337,\ ptcp:1337:[fe80::1234%eth0],pssl:1337:[fe80::1],ssl:10.10.10.1,tcp:127.0.0.1:1337,tcp:[fe80::1234%eth0],tcp:[fe80::1]:1337,\ unix:/some/path,punix:other/path ExecStart=/usr/bin/ovs-vsctl set Controller br0 connection-mode=out-of-band ExecStart=/usr/bin/ovs-vsctl set Controller br0 external-ids:netplan/connection-mode=out-of-band '''}, 'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'br0.network': ND_EMPTY % ('br0', 'ipv6')}) def test_bridge_controller_invalid_target(self): err = self.generate('''network: version: 2 bridges: br0: openvswitch: controller: addresses: [ptcp] ''', expect_fail=True) self.assertIn("Unsupported OVS controller target: ptcp", err) self.assert_ovs({}) self.assert_networkd({}) def test_bridge_controller_invalid_target_ip(self): err = self.generate('''network: version: 2 bridges: br0: openvswitch: controller: addresses: ["tcp:[fe80:1234%eth0]"] ''', expect_fail=True) self.assertIn("Unsupported OVS controller target: tcp:[fe80:1234%eth0]", err) self.assert_ovs({}) self.assert_networkd({}) def test_bridge_controller_invalid_target_port(self): err = self.generate('''network: version: 2 bridges: br0: openvswitch: controller: addresses: [ptcp:65536] ''', expect_fail=True) self.assertIn("Unsupported OVS controller target: ptcp:65536", err) self.assert_ovs({}) self.assert_networkd({}) def test_bridge_controller_invalid_connection_mode(self): err = self.generate('''network: version: 2 bridges: br0: openvswitch: controller: connection-mode: INVALID ''', expect_fail=True) self.assertIn("Value of 'connection-mode' needs to be 'in-band' or 'out-of-band'", err) self.assert_ovs({}) self.assert_networkd({}) def test_bridge_controller_connection_mode_invalid_interface_type(self): err = self.generate('''network: version: 2 bonds: mybond: openvswitch: controller: connection-mode: in-band ''', expect_fail=True) self.assertIn("Key 'controller.connection-mode' is only valid for interface type 'Open vSwitch bridge'", err) self.assert_ovs({}) self.assert_networkd({}) def test_bridge_controller_addresses_invalid_interface_type(self): err = self.generate('''network: version: 2 bonds: mybond: openvswitch: controller: addresses: [unix:/some/socket] ''', expect_fail=True) self.assertIn("Key 'controller.addresses' is only valid for interface type 'Open vSwitch bridge'", err) self.assert_ovs({}) self.assert_networkd({}) def test_global_ssl(self): self.generate('''network: version: 2 openvswitch: ssl: ca-cert: /another/path certificate: /some/path private-key: /key/path ''') self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({}) def test_missing_ssl(self): err = self.generate('''network: version: 2 bridges: br0: openvswitch: controller: addresses: [ssl:10.10.10.1] openvswitch: ssl: {} ''', expect_fail=True) self.assertIn("ERROR: Open vSwitch bridge controller target 'ssl:10.10.10.1' needs SSL configuration, but global \ 'openvswitch.ssl' settings are not set", err) self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({}) def test_global_ports(self): err = self.generate('''network: version: 2 openvswitch: ports: - [patch0-1, patch1-0] ''', expect_fail=True) self.assertIn('patch0-1: OpenVSwitch patch port needs to be assigned to a bridge/bond', err) self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({}) def test_few_ports(self): err = self.generate('''network: version: 2 openvswitch: ports: - [patch0-1] ''', expect_fail=True) self.assertIn("An Open vSwitch peer port sequence must have exactly two entries", err) self.assertIn("- [patch0-1]", err) self.assert_ovs({}) self.assert_networkd({}) def test_many_ports(self): err = self.generate('''network: version: 2 openvswitch: ports: - [patch0-1, "patchx", patchy] ''', expect_fail=True) self.assertIn("An Open vSwitch peer port sequence must have exactly two entries", err) self.assertIn("- [patch0-1, \"patchx\", patchy]", err) self.assert_ovs({}) self.assert_networkd({}) def test_ovs_invalid_port(self): err = self.generate('''network: version: 2 openvswitch: ports: - [patchx, patchy] - [patchx, patchz] ''', expect_fail=True) self.assertIn("Open vSwitch port 'patchx' is already assigned to peer 'patchy'", err) self.assert_ovs({}) self.assert_networkd({}) def test_ovs_invalid_peer(self): err = self.generate('''network: version: 2 openvswitch: ports: - [patchx, patchy] - [patchz, patchx] ''', expect_fail=True) self.assertIn("Open vSwitch port 'patchx' is already assigned to peer 'patchy'", err) self.assert_ovs({}) self.assert_networkd({}) def test_bridge_auto_ovs_backend(self): self.generate('''network: version: 2 ethernets: eth1: {} eth2: {} bonds: bond0: interfaces: [eth1, eth2] openvswitch: {} bridges: br0: addresses: [192.170.1.1/24] interfaces: [bond0] ''') self.assert_ovs({'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, 'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': '''Requires=netplan-ovs-br0.service After=netplan-ovs-br0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), 'bond0.network': ND_EMPTY % ('bond0', 'no'), 'eth1.network': '''[Match] Name=eth1 [Network] LinkLocalAddressing=no Bond=bond0 ''', 'eth2.network': '''[Match] Name=eth2 [Network] LinkLocalAddressing=no Bond=bond0 '''}) def test_bond_auto_ovs_backend(self): self.generate('''network: version: 2 ethernets: eth0: {} bonds: bond0: interfaces: [eth0, patchy] bridges: br0: addresses: [192.170.1.1/24] interfaces: [bond0] br1: addresses: [2001:FFfe::1/64] interfaces: [patchx] openvswitch: ports: - [patchx, patchy] ''') self.assert_ovs({'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patchx -- set Interface patchx type=patch options:peer=patchy ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, 'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': '''Requires=netplan-ovs-br0.service After=netplan-ovs-br0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 patchy eth0 -- set Interface patchy type=patch options:peer=patchx ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off '''}, 'patchx.service': OVS_VIRTUAL % {'iface': 'patchx', 'extra': '''Requires=netplan-ovs-br1.service After=netplan-ovs-br1.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patchx external-ids:netplan=true '''}, 'patchy.service': OVS_VIRTUAL % {'iface': 'patchy', 'extra': '''Requires=netplan-ovs-bond0.service After=netplan-ovs-bond0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), 'br1.network': ND_WITHIP % ('br1', '2001:FFfe::1/64'), 'bond0.network': ND_EMPTY % ('bond0', 'no'), 'patchx.network': ND_EMPTY % ('patchx', 'no'), 'patchy.network': ND_EMPTY % ('patchy', 'no'), 'eth0.network': '[Match]\nName=eth0\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n'}) def test_patch_ports(self): self.generate('''network: version: 2 openvswitch: ports: - [patch0-1, patch1-0] bridges: br0: addresses: [192.168.1.1/24] interfaces: [patch0-1] br1: addresses: [192.168.1.2/24] interfaces: [patch1-0] ''') self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 patch0-1 -- set Interface patch0-1 type=patch options:peer=patch1-0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patch1-0 -- set Interface patch1-0 type=patch options:peer=patch0-1 ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, 'patch0-1.service': OVS_VIRTUAL % {'iface': 'patch0-1', 'extra': '''Requires=netplan-ovs-br0.service After=netplan-ovs-br0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patch0-1 external-ids:netplan=true '''}, 'patch1-0.service': OVS_VIRTUAL % {'iface': 'patch1-0', 'extra': '''Requires=netplan-ovs-br1.service After=netplan-ovs-br1.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), 'br1.network': ND_WITHIP % ('br1', '192.168.1.2/24'), 'patch0-1.network': ND_EMPTY % ('patch0-1', 'no'), 'patch1-0.network': ND_EMPTY % ('patch1-0', 'no')}) def test_fake_vlan_bridge_setup(self): self.generate('''network: version: 2 bridges: br0: addresses: [192.168.1.1/24] openvswitch: {} vlans: br0.100: id: 100 link: br0 openvswitch: {} ''') self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, 'br0.100.service': OVS_VIRTUAL % {'iface': 'br0.100', 'extra': '''Requires=netplan-ovs-br0.service After=netplan-ovs-br0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), 'br0.100.network': ND_EMPTY % ('br0.100', 'ipv6')}) def test_implicit_fake_vlan_bridge_setup(self): # Test if, when a VLAN is added to an OVS bridge, netplan will # implicitly assume the vlan should be done via OVS as well self.generate('''network: version: 2 bridges: br0: addresses: [192.168.1.1/24] openvswitch: {} vlans: br0.100: id: 100 link: br0 ''') self.assert_ovs({'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, 'br0.100.service': OVS_VIRTUAL % {'iface': 'br0.100', 'extra': '''Requires=netplan-ovs-br0.service After=netplan-ovs-br0.service [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), 'br0.100.network': ND_EMPTY % ('br0.100', 'ipv6')}) def test_invalid_device_type(self): err = self.generate('''network: version: 2 ethernets: eth0: openvswitch: {} ''', expect_fail=True) self.assertIn('eth0: This device type is not supported with the OpenVSwitch backend', err) self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({}) def test_bridge_non_ovs_bond(self): self.generate('''network: version: 2 ethernets: eth0: {} eth1: {} bonds: non-ovs-bond: interfaces: [eth0, eth1] bridges: ovs-br: interfaces: [non-ovs-bond] openvswitch: {} ''') self.assert_ovs({'ovs-br.service': OVS_VIRTUAL % {'iface': 'ovs-br', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs-br ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs-br non-ovs-bond ''' + OVS_BR_DEFAULT % {'iface': 'ovs-br'}}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) # Confirm that the networkd config is still sane self.assert_networkd({'non-ovs-bond.network': ND_EMPTY % ('non-ovs-bond', 'no') + 'Bridge=ovs-br\n', 'eth1.network': (ND_EMPTY % ('eth1', 'no')).replace('ConfigureWithoutCarrier=yes', 'Bond=non-ovs-bond'), 'eth0.network': (ND_EMPTY % ('eth0', 'no')).replace('ConfigureWithoutCarrier=yes', 'Bond=non-ovs-bond'), 'ovs-br.network': ND_EMPTY % ('ovs-br', 'ipv6'), 'non-ovs-bond.netdev': '[NetDev]\nName=non-ovs-bond\nKind=bond\n'}) def test_ovs_invalid_networkd_config(self): err = self.generate('''network: version: 2 bridges: br0: openvswitch: {} ipv6-address-generation: stable-privacy ''', expect_fail=True) self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assertIn('br0: ipv6-address-generation mode is not supported by networkd', err) def test_ovs_duplicates_when_parser_needs_second_pass(self): ''' Test case for LP: #2007682 The generator shouldn't generate duplicates when the parser needs a second pass. ''' self.generate('''network: version: 2 bridges: br123: openvswitch: protocols: - OpenFlow10 - OpenFlow11 - OpenFlow12 controller: addresses: - tcp:127.0.0.1:6653 interfaces: - nic1 ethernets: nic1: {} ''') self.assert_ovs({'br123.service': OVS_VIRTUAL % {'iface': 'br123', 'extra': ''' [Service] Type=oneshot TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br123 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br123 nic1 ''' + OVS_BR_DEFAULT % {'iface': 'br123'} + ('\ ExecStart=/usr/bin/ovs-vsctl set Bridge br123 protocols=OpenFlow10,OpenFlow11,OpenFlow12\n\ ExecStart=/usr/bin/ovs-vsctl set Bridge br123 external-ids:netplan/protocols=OpenFlow10,OpenFlow11,OpenFlow12\n\ ExecStart=/usr/bin/ovs-vsctl set-controller br123 tcp:127.0.0.1:6653\n\ ExecStart=/usr/bin/ovs-vsctl set Bridge br123 external-ids:netplan/global/set-controller=tcp:127.0.0.1:6653\n\ ')}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) def test_both_ports_with_same_name(self): err = self.generate('''network: version: 2 openvswitch: ports: - [portname, portname] ''', expect_fail=True) self.assertIn('Open vSwitch patch ports must be of different name', err) netplan-1.0/tests/generator/test_passthrough.py000066400000000000000000000225601457004145200221320ustar00rootroot00000000000000# # Tests for passthrough config generated via netplan # # Copyright (C) 2021 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .base import TestBase # No passthrough mode (yet) for systemd-networkd class TestNetworkd(TestBase): pass class TestNetworkManager(TestBase): def test_passthrough_basic(self): self.generate('''network: version: 2 ethernets: NM-87749f1d-334f-40b2-98d4-55db58965f5f: renderer: NetworkManager match: {} networkmanager: uuid: 87749f1d-334f-40b2-98d4-55db58965f5f name: some NM id passthrough: connection.uuid: 87749f1d-334f-40b2-98d4-55db58965f5f connection.type: ethernet connection.permissions: ""''') self.assert_nm({'NM-87749f1d-334f-40b2-98d4-55db58965f5f': '''[connection] id=some NM id type=ethernet uuid=87749f1d-334f-40b2-98d4-55db58965f5f #Netplan: passthrough setting permissions= [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore '''}, '''[device-netplan.ethernets.NM-87749f1d-334f-40b2-98d4-55db58965f5f] match-device=type:ethernet managed=1\n\n''') def test_passthrough_wifi(self): self.generate('''network: version: 2 wifis: NM-87749f1d-334f-40b2-98d4-55db58965f5f: renderer: NetworkManager match: {} access-points: "SOME-SSID": networkmanager: uuid: 87749f1d-334f-40b2-98d4-55db58965f5f name: myid with spaces passthrough: connection.permissions: "" wifi.ssid: SOME-SSID "OTHER-SSID": hidden: true''') self.assert_nm({'NM-87749f1d-334f-40b2-98d4-55db58965f5f-SOME-SSID': '''[connection] id=myid with spaces type=wifi uuid=87749f1d-334f-40b2-98d4-55db58965f5f #Netplan: passthrough setting permissions= [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=SOME-SSID mode=infrastructure ''', 'NM-87749f1d-334f-40b2-98d4-55db58965f5f-OTHER-SSID': '''[connection] id=netplan-NM-87749f1d-334f-40b2-98d4-55db58965f5f-OTHER-SSID type=wifi [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=OTHER-SSID mode=infrastructure hidden=true '''}, '''[device-netplan.wifis.NM-87749f1d-334f-40b2-98d4-55db58965f5f] match-device=type:wifi managed=1\n\n''') def test_passthrough_type_nm_devices(self): self.generate('''network: nm-devices: NM-87749f1d-334f-40b2-98d4-55db58965f5f: renderer: NetworkManager match: {} networkmanager: passthrough: connection.uuid: 87749f1d-334f-40b2-98d4-55db58965f5f connection.type: dummy''') # wokeignore:rule=dummy self.assert_nm({'NM-87749f1d-334f-40b2-98d4-55db58965f5f': '''[connection] id=netplan-NM-87749f1d-334f-40b2-98d4-55db58965f5f #Netplan: passthrough setting uuid=87749f1d-334f-40b2-98d4-55db58965f5f #Netplan: passthrough setting type=dummy # wokeignore:rule=dummy [ipv4] method=link-local [ipv6] method=ignore '''}, '''[device-netplan.nm-devices.NM-87749f1d-334f-40b2-98d4-55db58965f5f] match-device=type:dummy # wokeignore:rule=dummy managed=1\n\n''') def test_passthrough_dotted_group(self): self.generate('''network: nm-devices: dotted-group-test: renderer: NetworkManager match: {} networkmanager: passthrough: connection.type: "wireguard" wireguard-peer.some-key.endpoint: 1.2.3.4''') self.assert_nm({'dotted-group-test': '''[connection] id=netplan-dotted-group-test #Netplan: passthrough setting type=wireguard [ipv4] method=link-local [ipv6] method=ignore [wireguard-peer.some-key] #Netplan: passthrough setting endpoint=1.2.3.4 '''}, '''[device-netplan.nm-devices.dotted-group-test] match-device=type:wireguard managed=1\n\n''') def test_passthrough_dotted_key(self): self.generate('''network: ethernets: dotted-key-test: renderer: NetworkManager match: {} networkmanager: passthrough: tc.qdisc.root: something tc.qdisc.fff1: ":abc" tc.filters.test: "test"''') self.assert_nm({'dotted-key-test': '''[connection] id=netplan-dotted-key-test type=ethernet [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore [tc] #Netplan: passthrough setting qdisc.root=something #Netplan: passthrough setting qdisc.fff1=:abc #Netplan: passthrough setting filters.test=test '''}, '''[device-netplan.ethernets.dotted-key-test] match-device=type:ethernet managed=1\n\n''') def test_passthrough_unsupported_setting(self): self.generate('''network: wifis: test: renderer: NetworkManager match: {} access-points: "SOME-SSID": # implicit "mode: infrasturcutre" networkmanager: passthrough: wifi.mode: "mesh"''') self.assert_nm({'test-SOME-SSID': '''[connection] id=netplan-test-SOME-SSID type=wifi [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=SOME-SSID #Netplan: passthrough override mode=mesh '''}, '''[device-netplan.wifis.test] match-device=type:wifi managed=1\n\n''') def test_passthrough_empty_group(self): self.generate('''network: ethernets: test: renderer: NetworkManager match: {} networkmanager: passthrough: proxy._: ""''') self.assert_nm({'test': '''[connection] id=netplan-test type=ethernet [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore [proxy] '''}, '''[device-netplan.ethernets.test] match-device=type:ethernet managed=1\n\n''') def test_passthrough_interface_rename_existing_id(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: # This is the original netdef, generating "netplan-eth0.nmconnection" eth0: dhcp4: true # This is the override netdef, modifying match.original_name (i.e. interface-name) # it should still generate a "netplan-eth0.nmconnection" file (not netplan-eth33.nmconnection). eth0: renderer: NetworkManager dhcp4: true match: name: "eth33" networkmanager: uuid: 626dd384-8b3d-3690-9511-192b2c79b3fd name: "netplan-eth0" ''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet uuid=626dd384-8b3d-3690-9511-192b2c79b3fd interface-name=eth33 [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=ignore '''}) def test_passthrough_ip6_privacy_default(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: dhcp4: true dhcp6: true networkmanager: uuid: 626dd384-8b3d-3690-9511-192b2c79b3fd name: "netplan-eth0" passthrough: "ipv6.ip6-privacy": "-1" ''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet uuid=626dd384-8b3d-3690-9511-192b2c79b3fd interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=auto '''}) def test_passthrough_empty_keyfile_group(self): out = self.generate('''network: wifis: wlan0: access-points: "SSID": networkmanager: name: connection_name passthrough: itsmissingadot: abc nm-devices: device0: networkmanager: name: connection_name passthrough: connection.type: vpn itsmissingadot: abc renderer: NetworkManager''', expect_fail=True, skip_generated_yaml_validation=True) self.assertIn("NetworkManager: passthrough key 'itsmissingadot' format is invalid, should be 'group.key'", out) def test_passthrough_wifi_without_network_manager(self): out = self.generate('''network: wifis: wlan0: access-points: "SSID": networkmanager: name: connection_name passthrough: new.option: abc''', expect_fail=True, skip_generated_yaml_validation=True) self.assertIn("wlan0: networkmanager backend settings found but renderer is not NetworkManager", out) def test_passthrough_wifi_empty_group_with_network_manager(self): out = self.generate('''network: wifis: wlan0: renderer: NetworkManager access-points: "SSID": networkmanager: name: connection_name passthrough: itsmissingadot: abc''', skip_generated_yaml_validation=True) self.assertIn("NetworkManager: passthrough key 'itsmissingadot' format is invalid, should be 'group.key'", out) def test_passthrough_empty_keyfile_group_only(self): out = self.generate('''network: nm-devices: device0: networkmanager: name: connection_name passthrough: itsmissingadot: abc renderer: NetworkManager''', expect_fail=True, skip_generated_yaml_validation=True) self.assertIn("device0: network type 'nm-devices:' needs to provide a 'connection.type' via passthrough", out) netplan-1.0/tests/generator/test_routing.py000066400000000000000000000765221457004145200212610ustar00rootroot00000000000000# # Routing / IP rule tests for netplan generator # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .base import TestBase, ND_VLAN, ND_DHCP4, ND_EMPTY class TestNetworkd(TestBase): def test_route_invalid_family_to(self): err = self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: abc/24 via: 192.168.14.20''', expect_fail=True) self.assertIn("Error in network definition: invalid IP family '-1'", err) def test_route_v4_single(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 Metric=100 '''}) def test_route_v4_single_mulit_parse(self): self.generate('''network: version: 2 bridges: br0: {interfaces: [engreen]} ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=no Address=192.168.14.2/24 Bridge=br0 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 Metric=100 ''', 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', 'br0.network': '''[Match]\nName=br0\n [Network] LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes '''}) def test_route_v4_multiple(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 8.8.0.0/16 via: 192.168.1.1 - to: 10.10.10.8 via: 192.168.1.2 metric: 5000 - to: 11.11.11.0/24 via: 192.168.1.3 metric: 9999 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=8.8.0.0/16 Gateway=192.168.1.1 [Route] Destination=10.10.10.8 Gateway=192.168.1.2 Metric=5000 [Route] Destination=11.11.11.0/24 Gateway=192.168.1.3 Metric=9999 '''}) def test_route_v4_default(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.1.2/24"] routes: - to: default via: 192.168.1.1 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.1.2/24 [Route] Destination=0.0.0.0/0 Gateway=192.168.1.1 '''}) def test_route_v4_onlink(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 on-link: true metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 GatewayOnLink=true Metric=100 '''}) def test_route_v4_onlink_no(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 on-link: n metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 Metric=100 '''}) def test_route_v4_scope(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 scope: link metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Scope=link Metric=100 '''}) def test_route_v4_scope_redefine(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 scope: host via: 192.168.14.20 scope: link metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 Scope=link Metric=100 '''}) def test_route_v4_type_blackhole(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 type: blackhole metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 Type=blackhole Metric=100 '''}) def test_route_v4_type_redefine(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 type: prohibit via: 192.168.14.20 type: unicast metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 Metric=100 '''}) def test_route_v4_table(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 table: 201 metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 Metric=100 Table=201 '''}) def test_route_v4_from(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 from: 192.168.14.2 metric: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 PreferredSource=192.168.14.2 Metric=100 '''}) def test_route_v4_mtu(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 mtu: 1500 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 MTUBytes=1500 '''}) def test_route_v4_congestion_window(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 congestion-window: 16 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 InitialCongestionWindow=16 '''}) def test_route_v4_advertised_receive_window(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 advertised-receive-window: 16 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 InitialAdvertisedReceiveWindow=16 '''}) def test_route_v6_single(self): self.generate('''network: version: 2 ethernets: enblue: addresses: ["192.168.1.3/24"] routes: - to: 2001:dead:beef::2/64 via: 2001:beef:beef::1''') self.assert_networkd({'enblue.network': '''[Match] Name=enblue [Network] LinkLocalAddressing=ipv6 Address=192.168.1.3/24 [Route] Destination=2001:dead:beef::2/64 Gateway=2001:beef:beef::1 '''}) def test_route_v6_type(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 2001:dead:beef::2/64 via: 2001:beef:beef::1 type: prohibit''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=2001:dead:beef::2/64 Gateway=2001:beef:beef::1 Type=prohibit '''}) def test_route_v6_scope_host(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 2001:dead:beef::2/64 via: 2001:beef:beef::1 scope: host''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=2001:dead:beef::2/64 Gateway=2001:beef:beef::1 Scope=host '''}) def test_route_v6_multiple(self): self.generate('''network: version: 2 ethernets: enblue: addresses: ["192.168.1.3/24"] routes: - to: 2001:dead:beef::2/64 via: 2001:beef:beef::1 - to: 2001:f00f:f00f::fe/64 via: 2001:beef:feed::1 metric: 1024''') self.assert_networkd({'enblue.network': '''[Match] Name=enblue [Network] LinkLocalAddressing=ipv6 Address=192.168.1.3/24 [Route] Destination=2001:dead:beef::2/64 Gateway=2001:beef:beef::1 [Route] Destination=2001:f00f:f00f::fe/64 Gateway=2001:beef:feed::1 Metric=1024 '''}) def test_route_v6_default(self): self.generate('''network: version: 2 ethernets: enblue: addresses: ["2001:dead:beef::2/64"] routes: - to: default via: 2001:beef:beef::1''') self.assert_networkd({'enblue.network': '''[Match] Name=enblue [Network] LinkLocalAddressing=ipv6 Address=2001:dead:beef::2/64 [Route] Destination=::/0 Gateway=2001:beef:beef::1 '''}) def test_ip_rule_table(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routing-policy: - to: 10.10.10.0/24 table: 100 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [RoutingPolicyRule] To=10.10.10.0/24 Table=100 '''}) def test_ip_rule_priority(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routing-policy: - to: 10.10.10.0/24 priority: 99 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [RoutingPolicyRule] To=10.10.10.0/24 Priority=99 '''}) def test_ip_rule_fwmark(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routing-policy: - from: 10.10.10.0/24 mark: 50 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [RoutingPolicyRule] From=10.10.10.0/24 FirewallMark=50 '''}) def test_ip_rule_tos(self): self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routing-policy: - to: 10.10.10.0/24 type-of-service: 250 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [RoutingPolicyRule] To=10.10.10.0/24 TypeOfService=250 '''}) def test_use_routes(self): """[networkd] Validate config generation when use-routes DHCP override is used""" self.generate('''network: version: 2 ethernets: engreen: dhcp4: true dhcp4-overrides: use-routes: false ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true UseRoutes=false '''}) def test_default_metric(self): """[networkd] Validate config generation when metric DHCP override is used""" self.generate('''network: version: 2 ethernets: engreen: dhcp4: true dhcp6: true dhcp4-overrides: route-metric: 3333 dhcp6-overrides: route-metric: 3333 enred: dhcp4: true dhcp6: true ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=yes LinkLocalAddressing=ipv6 [DHCP] RouteMetric=3333 UseMTU=true ''', 'enred.network': '''[Match] Name=enred [Network] DHCP=yes LinkLocalAddressing=ipv6 [DHCP] RouteMetric=100 UseMTU=true '''}) def test_default_scope_link_lp1805038(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: true routes: - to: 10.96.0.0/24 enred: dhcp4: true routes: - to: 10.97.0.0/24 type: broadcast ''', skip_generated_yaml_validation=True) # scope: link is a default value in this case self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [Route] Destination=10.96.0.0/24 Scope=link [DHCP] RouteMetric=100 UseMTU=true ''', 'enred.network': '''[Match] Name=enred [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [Route] Destination=10.97.0.0/24 Scope=link Type=broadcast [DHCP] RouteMetric=100 UseMTU=true '''}) def test_type_local_lp1892272(self): self.generate('''network: version: 2 ethernets: engreen: dhcp4: true routes: - to: 0.0.0.0/0 type: local table: 99 - to: ::0/0 type: local table: 100 ''', skip_generated_yaml_validation=True) # scope: host is a default value in this case self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [Route] Destination=0.0.0.0/0 Scope=host Type=local Table=99 [Route] Destination=::0/0 Scope=host Type=local Table=100 [DHCP] RouteMetric=100 UseMTU=true '''}) def test_route_metric_rendering_lp2023681(self): """Validate metric rendering is unsigned (can render up to 4294967294, 4294967295 is used internally to define an unset value) """ self.generate('''network: version: 2 ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 metric: 4294967294 ''') self.assert_networkd({'engreen.network': '''[Match] Name=engreen [Network] LinkLocalAddressing=ipv6 Address=192.168.14.2/24 [Route] Destination=10.10.10.0/24 Gateway=192.168.14.20 Metric=4294967294 '''}) class TestNetworkManager(TestBase): def test_route_v4_single(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 metric: 100 ''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=10.10.10.0/24,192.168.14.20,100 [ipv6] method=ignore '''}) def test_route_v4_multiple(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 8.8.0.0/16 via: 192.168.1.1 metric: 5000 - to: 10.10.10.8 via: 192.168.1.2 - to: 11.11.11.0/24 via: 192.168.1.3 metric: 9999 ''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=8.8.0.0/16,192.168.1.1,5000 route2=10.10.10.8,192.168.1.2 route3=11.11.11.0/24,192.168.1.3,9999 [ipv6] method=ignore '''}) def test_route_v4_default(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: ["192.168.1.2/24"] routes: - to: default via: 192.168.1.1 ''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.1.2/24 route1=0.0.0.0/0,192.168.1.1 [ipv6] method=ignore '''}) def test_route_v6_single(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: enblue: addresses: ["2001:f00f:f00f::2/64"] routes: - to: 2001:dead:beef::2/64 via: 2001:beef:beef::1''') self.assert_nm({'enblue': '''[connection] id=netplan-enblue type=ethernet interface-name=enblue [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=manual address1=2001:f00f:f00f::2/64 ip6-privacy=0 route1=2001:dead:beef::2/64,2001:beef:beef::1 '''}) def test_route_v6_multiple(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: enblue: addresses: ["2001:f00f:f00f::2/64"] routes: - to: 2001:dead:beef::2/64 via: 2001:beef:beef::1 - to: 2001:dead:feed::2/64 via: 2001:beef:beef::2 metric: 1000''') self.assert_nm({'enblue': '''[connection] id=netplan-enblue type=ethernet interface-name=enblue [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=manual address1=2001:f00f:f00f::2/64 ip6-privacy=0 route1=2001:dead:beef::2/64,2001:beef:beef::1 route2=2001:dead:feed::2/64,2001:beef:beef::2,1000 '''}) def test_route_v6_default(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: enblue: addresses: ["2001:dead:beef::2/64"] routes: - to: default via: 2001:beef:beef::1''') self.assert_nm({'enblue': '''[connection] id=netplan-enblue type=ethernet interface-name=enblue [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=manual address1=2001:dead:beef::2/64 ip6-privacy=0 route1=::/0,2001:beef:beef::1 '''}) def test_routes_mixed(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: ["192.168.14.2/24", "2001:f00f::2/128"] routes: - to: 2001:dead:beef::2/64 via: 2001:beef:beef::1 metric: 997 - to: 8.8.0.0/16 via: 192.168.1.1 metric: 5000 - to: 10.10.10.8 via: 192.168.1.2 - to: 11.11.11.0/24 via: 192.168.1.3 metric: 9999 - to: 2001:f00f:f00f::fe/64 via: 2001:beef:feed::1 ''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=8.8.0.0/16,192.168.1.1,5000 route2=10.10.10.8,192.168.1.2 route3=11.11.11.0/24,192.168.1.3,9999 [ipv6] method=manual address1=2001:f00f::2/128 ip6-privacy=0 route1=2001:dead:beef::2/64,2001:beef:beef::1,997 route2=2001:f00f:f00f::fe/64,2001:beef:feed::1 '''}) def test_route_from(self): out = self.generate('''network: version: 2 ethernets: engreen: renderer: NetworkManager addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 from: 192.168.14.2 ''') self.assertEqual('', out) self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=10.10.10.0/24,192.168.14.20 route1_options=src=192.168.14.2 [ipv6] method=ignore '''}) self.assert_networkd({}) def test_route_onlink(self): out = self.generate('''network: version: 2 ethernets: engreen: renderer: NetworkManager addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.1.20 on-link: true ''') self.assertEqual('', out) self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=10.10.10.0/24,192.168.1.20 route1_options=onlink=true [ipv6] method=ignore '''}) self.assert_networkd({}) def test_route_table(self): out = self.generate('''network: version: 2 ethernets: engreen: renderer: NetworkManager addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.1.20 table: 31337 ''') self.assertEqual('', out) self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=10.10.10.0/24,192.168.1.20 route1_options=table=31337 [ipv6] method=ignore '''}) self.assert_networkd({}) def test_route_mtu(self): out = self.generate('''network: version: 2 ethernets: engreen: renderer: NetworkManager addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.1.20 mtu: 1500 ''') self.assertEqual('', out) self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=10.10.10.0/24,192.168.1.20 route1_options=mtu=1500 [ipv6] method=ignore '''}) self.assert_networkd({}) def test_route_congestion_window(self): out = self.generate('''network: version: 2 ethernets: engreen: renderer: NetworkManager addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.1.20 congestion-window: 16 ''') self.assertEqual('', out) self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=10.10.10.0/24,192.168.1.20 route1_options=initcwnd=16 [ipv6] method=ignore '''}) self.assert_networkd({}) def test_route_advertised_receive_window(self): out = self.generate('''network: version: 2 ethernets: engreen: renderer: NetworkManager addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.1.20 advertised-receive-window: 16 ''') self.assertEqual('', out) self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=10.10.10.0/24,192.168.1.20 route1_options=initrwnd=16 [ipv6] method=ignore '''}) self.assert_networkd({}) def test_route_options(self): out = self.generate('''network: version: 2 ethernets: engreen: renderer: NetworkManager addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.1.20 table: 31337 from: 192.168.14.2 on-link: true ''') self.assertEqual('', out) self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=10.10.10.0/24,192.168.1.20 route1_options=onlink=true,table=31337,src=192.168.14.2 [ipv6] method=ignore '''}) self.assert_networkd({}) def test_route_reject_type(self): err = self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.1.20 type: blackhole ''', expect_fail=True) self.assertIn('NetworkManager only supports unicast routes', err) self.assert_nm({}) self.assert_networkd({}) def test_route_reject_type_v6(self): err = self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: ["2001:f00f::2/128"] routes: - to: 2001:dead:beef::2/64 via: 2001:beef:beef::1 type: blackhole ''', expect_fail=True) self.assertIn('NetworkManager only supports unicast routes', err) self.assert_nm({}) self.assert_networkd({}) def test_use_routes_v4(self): """[NetworkManager] Validate config when use-routes DHCP4 override is used""" self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp4: true dhcp4-overrides: use-routes: false ''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=auto ignore-auto-routes=true never-default=true [ipv6] method=ignore '''}) def test_use_routes_v6(self): """[NetworkManager] Validate config when use-routes DHCP6 override is used""" self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp4: true dhcp6: true dhcp6-overrides: use-routes: false ''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=auto ip6-privacy=0 ignore-auto-routes=true never-default=true '''}) def test_default_metric_v4(self): """[NetworkManager] Validate config when setting a default metric for DHCPv4""" self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp4: true dhcp6: true dhcp4-overrides: route-metric: 4000 ''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=auto route-metric=4000 [ipv6] method=auto ip6-privacy=0 '''}) def test_default_metric_v6(self): """[NetworkManager] Validate config when setting a default metric for DHCPv6""" self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: dhcp4: true dhcp6: true dhcp6-overrides: route-metric: 5050 ''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=auto ip6-privacy=0 route-metric=5050 '''}) def test_add_routes_to_different_tables_from_multiple_files(self): """Test case for bug LP#2003061""" self.generate('''network: version: 2''', confs={'01-netcfg': '''network: version: 2 ethernets: eth0: dhcp4: true vlans: vlan100: id: 100 link: eth0''', '10-table1': '''network: version: 2 ethernets: {eth0: {dhcp4: true}} vlans: vlan100: id: 100 link: eth0 routing-policy: - from: 10.0.0.1 table: 1001 routes: - to: 0.0.0.0/0 via: 10.0.0.100 table: 1001''', '10-table2': '''network: version: 2 ethernets: {eth0: {dhcp4: true}} vlans: vlan100: id: 100 link: eth0 routing-policy: - from: 10.0.0.2 table: 1002 routes: - to: 0.0.0.0/0 via: 10.0.0.200 table: 1002'''}) self.assert_networkd({'eth0.network': (ND_DHCP4 % 'eth0').replace('\n[DHCP]', 'VLAN=vlan100\n\n[DHCP]'), 'vlan100.netdev': ND_VLAN % ('vlan100', 100), 'vlan100.network': ND_EMPTY % ('vlan100', 'ipv6') + ''' [Route] Destination=0.0.0.0/0 Gateway=10.0.0.100 Table=1001 [Route] Destination=0.0.0.0/0 Gateway=10.0.0.200 Table=1002 [RoutingPolicyRule] From=10.0.0.1 Table=1001 [RoutingPolicyRule] From=10.0.0.2 Table=1002 '''}) def test_add_duplicate_routes_from_multiple_files(self): """ Duplicate route should produce a single entry in the backend configuration""" self.generate('''network: version: 2''', confs={'01-netcfg': '''network: version: 2 ethernets: eth0: dhcp4: true vlans: vlan100: id: 100 link: eth0''', '10-table1': '''network: version: 2 ethernets: {eth0: {dhcp4: true}} vlans: vlan100: id: 100 link: eth0 routing-policy: - from: 10.0.0.1 table: 1001 routes: - to: 0.0.0.0/0 via: 10.0.0.100 table: 1001''', '10-table2': '''network: version: 2 ethernets: {eth0: {dhcp4: true}} vlans: vlan100: id: 100 link: eth0 routing-policy: - from: 10.0.0.2 table: 1002 routes: - to: 0.0.0.0/0 via: 10.0.0.100 table: 1001'''}) self.assert_networkd({'eth0.network': (ND_DHCP4 % 'eth0').replace('\n[DHCP]', 'VLAN=vlan100\n\n[DHCP]'), 'vlan100.netdev': ND_VLAN % ('vlan100', 100), 'vlan100.network': ND_EMPTY % ('vlan100', 'ipv6') + ''' [Route] Destination=0.0.0.0/0 Gateway=10.0.0.100 Table=1001 [RoutingPolicyRule] From=10.0.0.1 Table=1001 [RoutingPolicyRule] From=10.0.0.2 Table=1002 '''}) def test_route_metric_rendering_lp2023681(self): """Validate metric rendering is unsigned (can render up to 4294967294, 4294967295 is used internally to define an unset value) """ self.generate('''network: version: 2 renderer: NetworkManager ethernets: engreen: addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 metric: 4294967294 ''') self.assert_nm({'engreen': '''[connection] id=netplan-engreen type=ethernet interface-name=engreen [ethernet] wake-on-lan=0 [ipv4] method=manual address1=192.168.14.2/24 route1=10.10.10.0/24,192.168.14.20,4294967294 [ipv6] method=ignore '''}) netplan-1.0/tests/generator/test_tunnels.py000066400000000000000000001710041457004145200212510ustar00rootroot00000000000000# # Tests for tunnel devices config generated via netplan # # Copyright (C) 2018-2022 Canonical, Ltd. # Copyright (C) 2022 Datto, Inc. # Author: Mathieu Trudel-Lapierre # Author: Anthony Timmins # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import re from .base import TestBase, ND_WITHIPGW, ND_EMPTY, NM_WG, ND_WG, ND_VXLAN def prepare_config_for_mode(renderer, mode, key=None, ttl=None): config = """network: version: 2 renderer: {} """.format(renderer) if mode == "ip6gre" \ or mode == "ip6ip6" \ or mode == "vti6" \ or mode == "ipip6" \ or mode == "ip6gretap": local_ip = "fe80::dead:beef" remote_ip = "2001:fe:ad:de:ad:be:ef:1" else: local_ip = "10.10.10.10" remote_ip = "20.20.20.20" append_ttl = '\n ttl: {}'.format(ttl) if ttl else '' config += """ tunnels: tun0: mode: {} local: {} remote: {}{} addresses: [ 15.15.15.15/24 ] gateway4: 20.20.20.21 """.format(mode, local_ip, remote_ip, append_ttl) # Handle key/keys as str or dict as required by the test if type(key) is str: config += """ key: {} """.format(key) elif type(key) is dict: config += """ keys: input: {} output: {} """.format(key['input'], key['output']) return config def prepare_wg_config(listen=None, privkey=None, fwmark=None, peers=[], renderer="networkd"): config = '''network: version: 2 renderer: %s tunnels: wg0: mode: wireguard addresses: [15.15.15.15/24, 2001:de:ad:be:ef:ca:fe:1/128] gateway4: 20.20.20.21 ''' % renderer if privkey is not None: config += ' key: {}\n'.format(privkey) if fwmark is not None: config += ' mark: {}\n'.format(fwmark) if listen is not None: config += ' port: {}\n'.format(listen) if len(peers) > 0: config += ' peers:\n' for peer in peers: public_key = peer.get('public-key') peer.pop('public-key', None) shared_key = peer.get('shared-key') peer.pop('shared-key', None) pfx = ' - ' for k, v in peer.items(): config += '{}{}: {}\n'.format(pfx, k, v) pfx = ' ' if public_key or shared_key: config += '{}keys:\n'.format(pfx) if public_key: config += ' public: {}\n'.format(public_key) if shared_key: config += ' shared: {}\n'.format(shared_key) return config class _CommonParserErrors(): def test_fail_invalid_private_key(self): """[wireguard] Show an error for an invalid private key""" config = prepare_wg_config(listen=12345, privkey='invalid.key', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: wg0: invalid wireguard private key", out) def test_fail_invalid_public_key(self): """[wireguard] Show an error for an invalid private key""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': '/invalid.key', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: wg0: invalid wireguard public key", out) def test_fail_invalid_shared_key(self): """[wireguard] Show an error for an invalid pre shared key""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'shared-key': 'invalid.key', 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: wg0: invalid wireguard shared key", out) def test_fail_keepalive_2big(self): """[wireguard] Show an error if keepalive is too big""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 100500, 'endpoint': '1.2.3.4:5'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: wg0: keepalive must be 0-65535 inclusive.", out) def test_fail_keepalive_bogus(self): """[wireguard] Show an error if keepalive is not an int""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 'bogus', 'endpoint': '1.2.3.4:5'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: invalid unsigned int value 'bogus'", out) def test_fail_allowed_ips_prefix4(self): """[wireguard] Show an error if ipv4 prefix is too big""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/200, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': '1.2.3.4:5'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: invalid prefix length in address", out) def test_fail_allowed_ips_prefix6(self): """[wireguard] Show an error if ipv6 prefix too big""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/224"]', 'keepalive': 14, 'endpoint': '1.2.3.4:5'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: invalid prefix length in address", out) def test_fail_allowed_ips_noprefix4(self): """[wireguard] Show an error if ipv4 prefix is missing""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': '1.2.3.4:5'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: address \'0.0.0.0\' is missing /prefixlength", out) def test_fail_allowed_ips_noprefix6(self): """[wireguard] Show an error if ipv6 prefix is missing""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1"]', 'keepalive': 14, 'endpoint': '1.2.3.4:5'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: address '2001:fe:ad:de:ad:be:ef:1' is missing /prefixlength", out) def test_fail_allowed_ips_bogus(self): """[wireguard] Show an error if the address is completely bogus""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[302.302.302.302/24, "2001:fe:ad:de:ad:be:ef:1"]', 'keepalive': 14, 'endpoint': '1.2.3.4:5'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: malformed address \'302.302.302.302/24\', \ must be X.X.X.X/NN or X:X:X:X:X:X:X:X/NN", out) def test_fail_remote_no_port4(self): """[wireguard] Show an error if ipv4 remote endpoint lacks a port""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': '1.2.3.4'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: endpoint '1.2.3.4' is missing :port", out) def test_fail_remote_no_port6(self): """[wireguard] Show an error if ipv6 remote endpoint lacks a port""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': "2001:fe:ad:de:ad:be:ef:1"}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: invalid endpoint address or hostname", out) def test_fail_remote_no_port_hn(self): """[wireguard] Show an error if fqdn remote endpoint lacks a port""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': 'fq.dn'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: endpoint 'fq.dn' is missing :port", out) def test_fail_remote_big_port4(self): """[wireguard] Show an error if ipv4 remote endpoint port is too big""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': '1.2.3.4:100500'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: invalid port in endpoint '1.2.3.4:100500", out) def test_fail_ipv6_remote_noport(self): """[wireguard] Show an error for v6 remote endpoint without port""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': '"[2001:fe:ad:de:ad:be:ef:11]"'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("endpoint \'[2001:fe:ad:de:ad:be:ef:11]\' is missing :port", out) def test_fail_ipv6_remote_nobrace(self): """[wireguard] Show an error for v6 remote endpoint without closing brace""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': '"[2001:fe:ad:de:ad:be:ef:11"'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("invalid address in endpoint '[2001:fe:ad:de:ad:be:ef:11'", out) def test_fail_ipv6_remote_malformed(self): """[wireguard] Show an error for malformed-v6 remote endpoint""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': '"[2001:fe:badfilinad:be:ef]:11"'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("invalid endpoint address or hostname '[2001:fe:badfilinad:be:ef]:11", out) def test_fail_short_remote(self): """[wireguard] Show an error for too-short remote endpoint""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': 'ab'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: invalid endpoint address or hostname 'ab'", out) def test_fail_bogus_peer_key(self): """[wireguard] Show an error for a bogus key in a peer""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'bogus': 'true', 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: unknown key 'bogus'", out) def test_fail_no_public_key(self): """[wireguard] Show an error for missing public_key""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: wg0: a public key is required.", out) def test_empty_string_as_endpoint_should_be_ignored(self): """[wireguard] If the endpoint key is present but set to '' it should just be ignored""" config = prepare_wg_config(listen=12345, privkey='KPt9BzQjejRerEv8RMaFlpsD675gNexELOQRXt/AcH0=', peers=[{'public-key': 'rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': '\"\"'}], renderer=self.backend) self.generate(config, skip_generated_yaml_validation=True) def test_vxlan_port_range_fail(self): out = self.generate('''network: tunnels: vx0: mode: vxlan port-range: [1,2,3]''', expect_fail=True) self.assertIn("Expected exactly two values for port-range", out) def test_vxlan_port_min_wrong_type(self): out = self.generate('''network: tunnels: vx0: mode: vxlan port-range: [a,10]''', expect_fail=True) self.assertIn("invalid unsigned int value 'a'", out) def test_vxlan_port_max_wrong_type(self): out = self.generate('''network: tunnels: vx0: mode: vxlan port-range: [10,b]''', expect_fail=True) self.assertIn("invalid unsigned int value 'b'", out) def test_vxlan_flags_fail(self): out = self.generate('''network: tunnels: vx0: mode: vxlan notifications: [INVALID]''', expect_fail=True) self.assertIn("invalid value for notifications: 'INVALID'", out) def test_vxlan_missing_vni(self): out = self.generate('''network: version: 2 tunnels: vxlan1005: mode: vxlan''', expect_fail=True) self.assertIn('missing \'id\' property (VXLAN VNI)', out) def test_vxlan_oob_vni(self): out = self.generate('''network: version: 2 tunnels: vxlan1005: mode: vxlan id: 17000000''', expect_fail=True) self.assertIn('VXLAN \'id\' (VNI) must be in range [1..16777215]', out) def test_vxlan_oob_flow_label(self): out = self.generate('''network: version: 2 tunnels: vxlan1005: mode: vxlan id: 1005 flow-label: 1111111''', expect_fail=True) self.assertIn('VXLAN \'flow-label\' must be in range [0..1048575]', out) def test_vxlan_local_remote_ip_family_mismatch(self): out = self.generate('''network: version: 2 tunnels: vxlan1005: mode: vxlan id: 1005 local: 10.10.10.1 remote: fe80::3''', expect_fail=True) self.assertIn('\'local\' and \'remote\' must be of same IP family type', out) class _CommonTests(): def test_simple(self): """[wireguard] Validate generation of simple wireguard config""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', fwmark=42, peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'shared-key': '7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8=', 'endpoint': '1.2.3.4:5'}], renderer=self.backend) self.generate(config) if self.backend == 'networkd': self.assert_networkd({'wg0.netdev': ND_WG % ('=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', '''FwMark=42 [WireGuardPeer] PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 PersistentKeepalive=23 Endpoint=1.2.3.4:5 PresharedKey=7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8='''), 'wg0.network': ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', '20.20.20.21')}) elif self.backend == 'NetworkManager': self.assert_nm({'wg0.nmconnection': NM_WG % ('4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', '''fwmark=42 [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] persistent-keepalive=23 endpoint=1.2.3.4:5 preshared-key=7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= preshared-key-flags=0 allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24;''')}) def test_simple_multi_pass(self): """[wireguard] Validate generation of a wireguard config, which is parsed multiple times""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': '1.2.3.4:5'}], renderer=self.backend) config = config.replace('tunnels:', 'bridges: {br0: {interfaces: [wg0]}}\n tunnels:') self.generate(config) if self.backend == 'networkd': self.assert_networkd({'wg0.netdev': ND_WG % ('=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' [WireGuardPeer] PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 PersistentKeepalive=23 Endpoint=1.2.3.4:5'''), 'wg0.network': (ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', '20.20.20.21') + 'Bridge=br0\n') .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no'), 'br0.network': ND_EMPTY % ('br0', 'ipv6'), 'br0.netdev': '''[NetDev]\nName=br0\nKind=bridge\n'''}) elif self.backend == 'NetworkManager': self.assert_nm({'wg0.nmconnection': '''[connection] id=netplan-wg0 type=wireguard interface-name=wg0 slave-type=bridge # wokeignore:rule=slave master=br0 # wokeignore:rule=master [wireguard] private-key=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8= listen-port=12345 [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] persistent-keepalive=23 endpoint=1.2.3.4:5 allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24; [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=manual address1=2001:de:ad:be:ef:ca:fe:1/128 ip6-privacy=0 ''', 'br0.nmconnection': '''[connection] id=netplan-br0 type=bridge interface-name=br0 [ipv4] method=link-local [ipv6] method=ignore '''}) def test_2peers(self): """[wireguard] Validate generation of wireguard config with two peers""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': '1.2.3.4:5'}, { 'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG5=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': '1.2.3.4:5'}], renderer=self.backend) self.generate(config) if self.backend == 'networkd': self.assert_networkd({'wg0.netdev': ND_WG % ('=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' [WireGuardPeer] PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 PersistentKeepalive=23 Endpoint=1.2.3.4:5 [WireGuardPeer] PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG5= AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 PersistentKeepalive=23 Endpoint=1.2.3.4:5'''), 'wg0.network': ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', '20.20.20.21')}) elif self.backend == 'NetworkManager': self.assert_nm({'wg0.nmconnection': NM_WG % ('4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] persistent-keepalive=23 endpoint=1.2.3.4:5 allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24; [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG5=] persistent-keepalive=23 endpoint=1.2.3.4:5 allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24;''')}) def test_privatekeyfile(self): """[wireguard] Validate generation of another simple wireguard config""" config = prepare_wg_config(listen=12345, privkey='/tmp/test_private_key', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'shared-key': '/tmp/test_preshared_key', 'endpoint': '1.2.3.4:5'}], renderer=self.backend) if self.backend == 'networkd': self.generate(config) self.assert_networkd({'wg0.netdev': ND_WG % ('File=/tmp/test_private_key', '12345', ''' [WireGuardPeer] PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 PersistentKeepalive=23 Endpoint=1.2.3.4:5 PresharedKeyFile=/tmp/test_preshared_key'''), 'wg0.network': ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', '20.20.20.21')}) elif self.backend == 'NetworkManager': err = self.generate(config, expect_fail=True) self.assertIn('wg0: private key needs to be base64 encoded when using the NM backend', err) def test_ipv6_remote(self): """[wireguard] Validate generation of wireguard config with v6 remote endpoint""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': '"[2001:fe:ad:de:ad:be:ef:11]:5"'}], renderer=self.backend) self.generate(config) if self.backend == 'networkd': self.assert_networkd({'wg0.netdev': ND_WG % ('=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' [WireGuardPeer] PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 PersistentKeepalive=23 Endpoint=[2001:fe:ad:de:ad:be:ef:11]:5'''), 'wg0.network': ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', '20.20.20.21')}) elif self.backend == 'NetworkManager': self.assert_nm({'wg0.nmconnection': NM_WG % ('4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] persistent-keepalive=23 endpoint=[2001:fe:ad:de:ad:be:ef:11]:5 allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24;''')}) def test_vxlan(self): out = self.generate('''network: renderer: %(r)s version: 2 tunnels: vxlan1005: link: br0 mode: vxlan local: 10.10.10.1 remote: 224.0.0.5 id: 1005 port: 4789 ageing: 42 mac-learning: true limit: 37 arp-proxy: true short-circuit: true type-of-service: 292 ttl: 128 flow-label: 0 do-not-fragment: true notifications: [l2-miss, l3-miss] checksums: [udp, zero-udp6-tx, zero-udp6-rx, remote-tx, remote-rx] extensions: [group-policy, generic-protocol] port-range: [42, 442] neigh-suppress: false hairpin: false port-mac-learning: false bridges: br0: interfaces: [vxlan1005]''' % {'r': self.backend}) if self.backend == 'networkd': self.assert_networkd({'vxlan1005.netdev': ND_VXLAN % ('vxlan1005', 1005) + '''Group=224.0.0.5 Local=10.10.10.1 TOS=292 TTL=128 MacLearning=true FDBAgeingSec=42 MaximumFDBEntries=37 ReduceARPProxy=true L2MissNotification=true L3MissNotification=true RouteShortCircuit=true UDPChecksum=true UDP6ZeroChecksumTx=true UDP6ZeroChecksumRx=true RemoteChecksumTx=true RemoteChecksumRx=true GroupPolicyExtension=true GenericProtocolExtension=true DestinationPort=4789 PortRange=42-442 FlowLabel=0 IPDoNotFragment=true\n''', 'vxlan1005.network': '''[Match] Name=vxlan1005 [Network] LinkLocalAddressing=no ConfigureWithoutCarrier=yes Bridge=br0 [Bridge] HairPin=false Learning=false NeighborSuppression=false\n''', 'br0.network': '''[Match] Name=br0 [Network] LinkLocalAddressing=ipv6 ConfigureWithoutCarrier=yes VXLAN=vxlan1005\n''', 'br0.netdev': '''[NetDev] Name=br0 Kind=bridge\n'''}) elif self.backend == 'NetworkManager': self.assertIn('checksums/extensions/flow-lable/do-not-fragment are ' 'not supported by NetworkManager', out) self.assert_nm({'vxlan1005': '''[connection] id=netplan-vxlan1005 type=vxlan interface-name=vxlan1005 slave-type=bridge # wokeignore:rule=slave master=br0 # wokeignore:rule=master [bridge-port] hairpin-mode=false [vxlan] ageing=42 destination-port=4789 id=1005 learning=true limit=37 local=10.10.10.1 remote=224.0.0.5 proxy=true l2-miss=true l3-miss=true source-port-min=42 source-port-max=442 tos=292 ttl=128 rsc=true parent=br0 [ipv4] method=disabled [ipv6] method=ignore\n''', 'br0': '''[connection] id=netplan-br0 type=bridge interface-name=br0 [ipv4] method=link-local [ipv6] method=ignore\n'''}) def test_vxlan_maclearning_arpproxy_shortcircuit_true(self): self.generate('''network: renderer: %(r)s version: 2 ethernets: eth0: ignore-carrier: true tunnels: vxlan1005: link: eth0 mode: vxlan id: 1005 mac-learning: true arp-proxy: true short-circuit: true''' % {'r': self.backend}) if self.backend == 'networkd': self.assert_networkd({'vxlan1005.netdev': ND_VXLAN % ('vxlan1005', 1005) + '''MacLearning=true ReduceARPProxy=true RouteShortCircuit=true\n''', 'vxlan1005.network': ND_EMPTY % ('vxlan1005', 'ipv6'), 'eth0.network': ND_EMPTY % ('eth0', 'ipv6') + '''VXLAN=vxlan1005\n'''}) elif self.backend == 'NetworkManager': self.assert_nm({'vxlan1005': '''[connection] id=netplan-vxlan1005 type=vxlan interface-name=vxlan1005 [vxlan] id=1005 learning=true proxy=true rsc=true parent=eth0 [ipv4] method=disabled [ipv6] method=ignore\n''', 'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore\n'''}) def test_vxlan_maclearning_arpproxy_shortcircuit_false(self): self.generate('''network: renderer: %(r)s version: 2 ethernets: eth0: ignore-carrier: true tunnels: vxlan1005: link: eth0 mode: vxlan id: 1005 mac-learning: false arp-proxy: false short-circuit: false''' % {'r': self.backend}) if self.backend == 'networkd': self.assert_networkd({'vxlan1005.netdev': ND_VXLAN % ('vxlan1005', 1005) + '''MacLearning=false ReduceARPProxy=false RouteShortCircuit=false\n''', 'vxlan1005.network': ND_EMPTY % ('vxlan1005', 'ipv6'), 'eth0.network': ND_EMPTY % ('eth0', 'ipv6') + '''VXLAN=vxlan1005\n'''}) elif self.backend == 'NetworkManager': self.assert_nm({'vxlan1005': '''[connection] id=netplan-vxlan1005 type=vxlan interface-name=vxlan1005 [vxlan] id=1005 learning=false proxy=false rsc=false parent=eth0 [ipv4] method=disabled [ipv6] method=ignore\n''', 'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore\n'''}) # Execute the _CommonParserErrors only for one backend, to spare some test cycles class TestNetworkd(TestBase, _CommonTests, _CommonParserErrors): backend = 'networkd' def test_sit(self): """[networkd] Validate generation of SIT tunnels""" config = prepare_config_for_mode('networkd', 'sit') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=sit [Tunnel] Independent=true Local=10.10.10.10 Remote=20.20.20.20 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_sit_he(self): """[networkd] Validate generation of SIT tunnels (HE example)""" # Test specifically a config like one that would enable Hurricane # Electric IPv6 tunnels. config = '''network: version: 2 renderer: networkd ethernets: eth0: addresses: - 1.1.1.1/24 - "2001:cafe:face::1/64" # provided by HE as routed /64 gateway4: 1.1.1.254 tunnels: he-ipv6: mode: sit remote: 2.2.2.2 local: 1.1.1.1 addresses: - "2001:dead:beef::2/64" gateway6: "2001:dead:beef::1" ''' self.generate(config) self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Network] LinkLocalAddressing=ipv6 Address=1.1.1.1/24 Address=2001:cafe:face::1/64 Gateway=1.1.1.254 ''', 'he-ipv6.netdev': '''[NetDev] Name=he-ipv6 Kind=sit [Tunnel] Independent=true Local=1.1.1.1 Remote=2.2.2.2 ''', 'he-ipv6.network': '''[Match] Name=he-ipv6 [Network] LinkLocalAddressing=ipv6 Address=2001:dead:beef::2/64 Gateway=2001:dead:beef::1 ConfigureWithoutCarrier=yes '''}) def test_vti(self): """[networkd] Validate generation of VTI tunnels""" config = prepare_config_for_mode('networkd', 'vti') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=vti [Tunnel] Independent=true Local=10.10.10.10 Remote=20.20.20.20 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_vti_with_key_str(self): """[networkd] Validate generation of VTI tunnels with input/output keys""" config = prepare_config_for_mode('networkd', 'vti', key='1.1.1.1') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=vti [Tunnel] Independent=true Local=10.10.10.10 Remote=20.20.20.20 InputKey=1.1.1.1 OutputKey=1.1.1.1 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_vti_with_key_dict(self): """[networkd] Validate generation of VTI tunnels with key dict""" config = prepare_config_for_mode('networkd', 'vti', key={'input': 1234, 'output': 5678}) self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=vti [Tunnel] Independent=true Local=10.10.10.10 Remote=20.20.20.20 InputKey=1234 OutputKey=5678 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_vti_invalid_key(self): """[networkd] Validate VTI tunnel generation key handling""" config = prepare_config_for_mode('networkd', 'vti', key={'input': 42, 'output': 'invalid'}) out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: invalid tunnel key 'invalid'", out) def test_vti6(self): """[networkd] Validate generation of VTI6 tunnels""" config = prepare_config_for_mode('networkd', 'vti6') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=vti6 [Tunnel] Independent=true Local=fe80::dead:beef Remote=2001:fe:ad:de:ad:be:ef:1 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_vti6_with_key(self): """[networkd] Validate generation of VTI6 tunnels with input/output keys""" config = prepare_config_for_mode('networkd', 'vti6', key='1.1.1.1') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=vti6 [Tunnel] Independent=true Local=fe80::dead:beef Remote=2001:fe:ad:de:ad:be:ef:1 InputKey=1.1.1.1 OutputKey=1.1.1.1 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_vti6_invalid_key(self): """[networkd] Validate VTI6 tunnel generation key handling""" config = prepare_config_for_mode('networkd', 'vti6', key='invalid') out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: invalid tunnel key 'invalid'", out) def test_ipip6(self): """[networkd] Validate generation of IPIP6 tunnels""" config = prepare_config_for_mode('networkd', 'ipip6') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=ip6tnl [Tunnel] Independent=true Mode=ipip6 Local=fe80::dead:beef Remote=2001:fe:ad:de:ad:be:ef:1 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_ipip(self): """[networkd] Validate generation of IPIP tunnels""" config = prepare_config_for_mode('networkd', 'ipip', ttl=64) self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=ipip [Tunnel] Independent=true Local=10.10.10.10 Remote=20.20.20.20 TTL=64 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_isatap(self): """[networkd] Warning for ISATAP tunnel generation not supported""" config = prepare_config_for_mode('networkd', 'isatap') out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: ISATAP tunnel mode is not supported", out) def test_gre(self): """[networkd] Validate generation of GRE tunnels""" config = prepare_config_for_mode('networkd', 'gre') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=gre [Tunnel] Independent=true Local=10.10.10.10 Remote=20.20.20.20 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_ip6gre(self): """[networkd] Validate generation of IP6GRE tunnels""" config = prepare_config_for_mode('networkd', 'ip6gre', '33490175') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=ip6gre [Tunnel] Independent=true Local=fe80::dead:beef Remote=2001:fe:ad:de:ad:be:ef:1 InputKey=33490175 OutputKey=33490175 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_gretap(self): """[networkd] Validate generation of GRETAP tunnels""" config = prepare_config_for_mode('networkd', 'gretap') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=gretap [Tunnel] Independent=true Local=10.10.10.10 Remote=20.20.20.20 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_ip6gretap(self): """[networkd] Validate generation of IP6GRETAP tunnels""" config = prepare_config_for_mode('networkd', 'ip6gretap') self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 Kind=ip6gretap [Tunnel] Independent=true Local=fe80::dead:beef Remote=2001:fe:ad:de:ad:be:ef:1 ''', 'tun0.network': '''[Match] Name=tun0 [Network] LinkLocalAddressing=ipv6 Address=15.15.15.15/24 Gateway=20.20.20.21 ConfigureWithoutCarrier=yes '''}) def test_vxlan_port_range_swap(self): out = self.generate('''network: version: 2 tunnels: vx0: mode: vxlan id: 1005 remote: 10.0.0.5 port-range: [100, 10]''') self.assertIn("swapped invalid port-range order [MIN, MAX]", out) self.assert_networkd({'vx0.netdev': ND_VXLAN % ('vx0', 1005) + 'Remote=10.0.0.5\nPortRange=10-100\nIndependent=true\n', 'vx0.network': ND_EMPTY % ('vx0', 'ipv6')}) def test_vxlan_ip6_multicast(self): self.generate('''network: version: 2 tunnels: vx0: mode: vxlan id: 1005 remote: "ff42::dead:beef"''') self.assert_networkd({'vx0.netdev': ND_VXLAN % ('vx0', 1005) + 'Group=ff42::dead:beef\nIndependent=true\n', 'vx0.network': ND_EMPTY % ('vx0', 'ipv6')}) class TestNetworkManager(TestBase, _CommonTests): backend = 'NetworkManager' def test_fail_invalid_private_key_file(self): """[wireguard] Show an error for an invalid private key-file""" config = prepare_wg_config(listen=12345, privkey='/invalid.key', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("wg0: private key needs to be base64 encoded when using the NM backend", out) def test_fail_invalid_shared_key_file(self): """[wireguard] Show an error for an invalid pre shared key-file""" config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 14, 'shared-key': '/invalid.key', 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) out = self.generate(config, expect_fail=True) self.assertIn("wg0: shared key needs to be base64 encoded when using the NM backend", out) def test_isatap(self): """[NetworkManager] Validate ISATAP tunnel generation""" config = prepare_config_for_mode('NetworkManager', 'isatap') self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=4 local=10.10.10.10 remote=20.20.20.20 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_sit(self): """[NetworkManager] Validate generation of SIT tunnels""" config = prepare_config_for_mode('NetworkManager', 'sit') self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=3 local=10.10.10.10 remote=20.20.20.20 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_sit_he(self): """[NetworkManager] Validate generation of SIT tunnels (HE example)""" # Test specifically a config like one that would enable Hurricane # Electric IPv6 tunnels. config = '''network: version: 2 renderer: NetworkManager ethernets: eth0: addresses: - 1.1.1.1/24 - "2001:cafe:face::1/64" # provided by HE as routed /64 gateway4: 1.1.1.254 tunnels: he-ipv6: mode: sit remote: 2.2.2.2 local: 1.1.1.1 addresses: - "2001:dead:beef::2/64" gateway6: "2001:dead:beef::1" ''' self.generate(config) self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 [ethernet] wake-on-lan=0 [ipv4] method=manual address1=1.1.1.1/24 gateway=1.1.1.254 [ipv6] method=manual address1=2001:cafe:face::1/64 ip6-privacy=0 ''', 'he-ipv6': '''[connection] id=netplan-he-ipv6 type=ip-tunnel interface-name=he-ipv6 [ip-tunnel] mode=3 local=1.1.1.1 remote=2.2.2.2 [ipv4] method=disabled [ipv6] method=manual address1=2001:dead:beef::2/64 ip6-privacy=0 gateway=2001:dead:beef::1 '''}) def test_vti(self): """[NetworkManager] Validate generation of VTI tunnels""" config = prepare_config_for_mode('NetworkManager', 'vti') self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=5 local=10.10.10.10 remote=20.20.20.20 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_vti6(self): """[NetworkManager] Validate generation of VTI6 tunnels""" config = prepare_config_for_mode('NetworkManager', 'vti6') self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=9 local=fe80::dead:beef remote=2001:fe:ad:de:ad:be:ef:1 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_ip6ip6(self): """[NetworkManager] Validate generation of IP6IP6 tunnels""" config = prepare_config_for_mode('NetworkManager', 'ip6ip6') self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=6 local=fe80::dead:beef remote=2001:fe:ad:de:ad:be:ef:1 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_ipip(self): """[NetworkManager] Validate generation of IPIP tunnels""" config = prepare_config_for_mode('NetworkManager', 'ipip', ttl=64) self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=1 local=10.10.10.10 remote=20.20.20.20 ttl=64 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_gre(self): """[NetworkManager] Validate generation of GRE tunnels""" config = prepare_config_for_mode('NetworkManager', 'gre') self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=2 local=10.10.10.10 remote=20.20.20.20 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_gre_with_keys(self): """[NetworkManager] Validate generation of GRE tunnels with keys""" config = prepare_config_for_mode('NetworkManager', 'gre', key={'input': 1111, 'output': 5555}) self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=2 local=10.10.10.10 remote=20.20.20.20 input-key=1111 output-key=5555 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_ip6gre(self): """[NetworkManager] Validate generation of IP6GRE tunnels""" config = prepare_config_for_mode('NetworkManager', 'ip6gre') self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=8 local=fe80::dead:beef remote=2001:fe:ad:de:ad:be:ef:1 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_ip6gre_with_key(self): """[NetworkManager] Validate generation of IP6GRE tunnels with key""" config = prepare_config_for_mode('NetworkManager', 'ip6gre', key='9999') self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 type=ip-tunnel interface-name=tun0 [ip-tunnel] mode=8 local=fe80::dead:beef remote=2001:fe:ad:de:ad:be:ef:1 input-key=9999 output-key=9999 [ipv4] method=manual address1=15.15.15.15/24 gateway=20.20.20.21 [ipv6] method=ignore '''}) def test_vxlan_uuid(self): self.generate('''network: renderer: NetworkManager tunnels: vx0: mode: vxlan id: 42 link: id0 ethernets: id0: match: {name: 'someIface'}''') self.assert_networkd({}) # get assigned UUID from id0 connection with open(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/netplan-id0.nmconnection')) as f: m = re.search('uuid=([0-9a-fA-F-]{36})\n', f.read()) self.assertTrue(m) uuid = m.group(1) self.assertNotEqual(uuid, "00000000-0000-0000-0000-000000000000") self.assert_nm({'vx0': '''[connection] id=netplan-vx0 type=vxlan interface-name=vx0 [vxlan] id=42 parent=%s [ipv4] method=disabled [ipv6] method=ignore\n''' % uuid, 'id0': '''[connection] id=netplan-id0 type=ethernet uuid=%s interface-name=someIface [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore\n''' % uuid}) def test_wireguard_with_private_key_flags(self): self.generate('''network: version: 2 tunnels: wg-tunnel: renderer: NetworkManager addresses: - "10.20.30.1/24" ipv6-address-generation: "stable-privacy" mode: "wireguard" peers: - endpoint: "10.20.30.40:51820" keys: public: "M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=" allowed-ips: - "0.0.0.0/0" keys: private-key-flags: - agent-owned - not-saved - not-required''') self.assert_nm({'wg-tunnel': '''[connection] id=netplan-wg-tunnel type=wireguard interface-name=wg-tunnel [wireguard] private-key-flags=7 [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] endpoint=10.20.30.40:51820 allowed-ips=0.0.0.0/0; [ipv4] method=manual address1=10.20.30.1/24 [ipv6] method=manual addr-gen-mode=1 ip6-privacy=0 '''}) class TestConfigErrors(TestBase): def test_missing_mode(self): """Fail if tunnel mode is missing""" config = '''network: version: 2 tunnels: tun0: remote: 20.20.20.20 local: 10.10.10.10 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: missing or invalid 'mode' property for tunnel", out) def test_invalid_mode(self): """Ensure an invalid tunnel mode shows an error message""" config = prepare_config_for_mode('networkd', 'invalid') out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: tunnel mode 'invalid' is not supported", out) def test_malformed_tunnel_ip(self): """Fail if local/remote IP for tunnel are malformed""" config = '''network: version: 2 tunnels: tun0: mode: gre remote: 20.20.20.20 local: 10.10.1invalid ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: malformed address '10.10.1invalid', must be X.X.X.X or X:X:X:X:X:X:X:X", out) def test_cidr_tunnel_ip(self): """Fail if local/remote IP for tunnel include /prefix""" config = '''network: version: 2 tunnels: tun0: mode: gre remote: 20.20.20.20 local: 10.10.10.10/21 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: address '10.10.10.10/21' should not include /prefixlength", out) def test_missing_remote_ip(self): """Fail if remote IP is missing""" config = '''network: version: 2 tunnels: tun0: mode: gre local: 20.20.20.20 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: missing 'remote' property for tunnel", out) def test_invalid_ttl(self): """Fail if TTL not in range [1...255]""" config = '''network: version: 2 tunnels: tun0: mode: ipip local: 20.20.20.20 remote: 10.10.10.10 ttl: 300 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: 'ttl' property for tunnel must be in range [1...255]", out) def test_wrong_local_ip_for_mode_v4(self): """Show an error when an IPv6 local addr is used for an IPv4 tunnel mode""" config = '''network: version: 2 tunnels: tun0: mode: gre local: fe80::2 remote: 20.20.20.20 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: 'local' must be a valid IPv4 address for this tunnel type", out) def test_wrong_remote_ip_for_mode_v4(self): """Show an error when an IPv6 remote addr is used for an IPv4 tunnel mode""" config = '''network: version: 2 tunnels: tun0: mode: gre local: 10.10.10.10 remote: 2006::1 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: 'remote' must be a valid IPv4 address for this tunnel type", out) def test_wrong_local_ip_for_mode_v6(self): """Show an error when an IPv4 local addr is used for an IPv6 tunnel mode""" config = '''network: version: 2 tunnels: tun0: mode: ip6gre local: 10.10.10.10 remote: 2001::3 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: 'local' must be a valid IPv6 address for this tunnel type", out) def test_wrong_remote_ip_for_mode_v6(self): """Show an error when an IPv4 remote addr is used for an IPv6 tunnel mode""" config = '''network: version: 2 tunnels: tun0: mode: ip6gre local: 2001::face remote: 20.20.20.20 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: 'remote' must be a valid IPv6 address for this tunnel type", out) def test_malformed_keys(self): """Show an error if tunnel keys stanza is malformed""" config = '''network: version: 2 tunnels: tun0: mode: ipip local: 10.10.10.10 remote: 20.20.20.20 keys: - input: 1234 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: invalid type for 'key[s]': must be a scalar or mapping", out) def test_networkd_invalid_input_key_use(self): """[networkd] Show an error if input-key is used for a mode that does not support it""" config = '''network: version: 2 renderer: networkd tunnels: tun0: mode: ipip local: 10.10.10.10 remote: 20.20.20.20 keys: input: 1234 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: 'input-key' is not required for this tunnel type", out) def test_networkd_invalid_output_key_use(self): """[networkd] Show an error if output-key is used for a mode that does not support it""" config = '''network: version: 2 renderer: networkd tunnels: tun0: mode: ipip local: 10.10.10.10 remote: 20.20.20.20 keys: output: 1234 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: 'output-key' is not required for this tunnel type", out) def test_nm_invalid_input_key_use(self): """[NetworkManager] Show an error if input-key is used for a mode that does not support it""" config = '''network: version: 2 renderer: NetworkManager tunnels: tun0: mode: ipip local: 10.10.10.10 remote: 20.20.20.20 keys: input: 1234 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: 'input-key' is not required for this tunnel type", out) def test_nm_invalid_output_key_use(self): """[NetworkManager] Show an error if output-key is used for a mode that does not support it""" config = '''network: version: 2 renderer: NetworkManager tunnels: tun0: mode: ipip local: 10.10.10.10 remote: 20.20.20.20 keys: output: 1234 ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: 'output-key' is not required for this tunnel type", out) def test_vxlan_eth_with_route(self): """[networkd] Validate that VXLAN= is in the right section (LP#2000713)""" config = '''network: version: 2 renderer: networkd ethernets: eth0: routes: - to: 10.20.30.40/32 via: 10.20.30.1 tunnels: vxlan1: mode: vxlan id: 1 link: eth0 ''' self.generate(config) self.assert_networkd({'eth0.network': '''[Match] Name=eth0 [Network] LinkLocalAddressing=ipv6 VXLAN=vxlan1 [Route] Destination=10.20.30.40/32 Gateway=10.20.30.1 ''', 'vxlan1.netdev': (ND_VXLAN % ('vxlan1', 1)).strip(), 'vxlan1.network': ND_EMPTY % ('vxlan1', 'ipv6')}) def test_wireguard_wrong_private_key_flag(self): config = '''network: version: 2 tunnels: wg-tunnel: renderer: NetworkManager mode: "wireguard" keys: private-key-flags: - agent-owned - not-required - it-doesnt-exist ''' out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: Key flag 'it-doesnt-exist' is not supported.", out) netplan-1.0/tests/generator/test_veths.py000066400000000000000000000123631457004145200207140ustar00rootroot00000000000000# # Tests for Virtual Ethernet (veth) devices config generated via netplan # # Copyright (C) 2023 Canonical, Ltd. # Author: Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .base import ND_VETH, ND_EMPTY, TestBase class _CommonTests(): def test_missing_peer_key_should_fail(self): out = self.generate('''network: version: 2 renderer: %(r)s virtual-ethernets: veth0: {}''' % {'r': self.backend}, expect_fail=True) self.assertIn('virtual-ethernet missing \'peer\' property', out) def test_veth_peer_is_not_a_veth_interface(self): out = self.generate('''network: version: 2 renderer: %(r)s virtual-ethernets: veth0: peer: eth0 ethernets: eth0: {} ''' % {'r': self.backend}, expect_fail=True) self.assertIn('\'eth0\' is not a virtual-ethernet interface', out) def test_veth_peer_is_anothers_interface_peer_already(self): out = self.generate('''network: version: 2 renderer: %(r)s virtual-ethernets: veth2: peer: veth1 veth0: peer: veth1 veth1: peer: veth2''' % {'r': self.backend}, expect_fail=True) self.assertIn('virtual-ethernet peer \'veth1\' is another virtual-ethernet\'s (veth2) peer already', out) def test_veth_peer_is_itself(self): out = self.generate('''network: version: 2 renderer: %(r)s virtual-ethernets: veth1: peer: veth1''' % {'r': self.backend}, expect_fail=True) self.assertIn('virtual-ethernet peer cannot be itself', out) def test_basic(self): self.generate('''network: version: 2 renderer: %(r)s virtual-ethernets: veth0: peer: veth1 veth1: peer: veth0''' % {'r': self.backend}) if self.backend == 'NetworkManager': self.assert_nm({'veth0': '''[connection] id=netplan-veth0 type={} interface-name=veth0 [veth] peer=veth1 [ipv4] method=link-local [ipv6] method=ignore '''.format(('veth')), 'veth1': '''[connection] id=netplan-veth1 type={} interface-name=veth1 [veth] peer=veth0 [ipv4] method=link-local [ipv6] method=ignore '''.format(('veth'))}) if self.backend == 'networkd': self.assert_networkd({'veth0.network': ND_EMPTY % ('veth0', 'ipv6'), 'veth1.network': ND_EMPTY % ('veth1', 'ipv6'), 'veth0.netdev': ND_VETH % ('veth0', 'veth1')}) class NetworkManager(TestBase, _CommonTests): backend = 'NetworkManager' def test_veth_peer_is_not_a_veth_interface_validation_stage(self): out = self.generate('''network: version: 2 renderer: NetworkManager virtual-ethernets: veth0: peer: eth0''', confs={'b': '''network: renderer: NetworkManager ethernets: eth0: {}'''}, expect_fail=True) self.assertIn('\'eth0\' is not a virtual-ethernet interface', out) def test_veth_peer_has_no_peer_itself(self): out = self.generate('''network: version: 2 renderer: NetworkManager virtual-ethernets: veth0: peer: veth1 veth1: peer: abc''', expect_fail=True) self.assertIn('virtual-ethernet peer \'veth1\' does not have a peer itself', out) def test_veth_peer_has_no_peer_itself_validation_stage(self): out = self.generate('''network: version: 2 renderer: NetworkManager virtual-ethernets: veth0: peer: veth1''', confs={'b': '''network: version: 2 renderer: NetworkManager virtual-ethernets: veth1: peer: asd'''}, expect_fail=True) self.assertIn('virtual-ethernet peer \'veth1\' does not have a peer itself', out) def test_veth_peer_is_anothers_interface_peer_already_validation_stage(self): out = self.generate('''network: version: 2 renderer: NetworkManager virtual-ethernets: veth0: peer: veth1 ''', confs={'b': '''network: renderer: NetworkManager virtual-ethernets: veth1: peer: veth2 veth2: peer: veth1'''}, expect_fail=True) self.assertIn('virtual-ethernet peer \'veth1\' is another virtual-ethernet\'s (veth2) peer already', out) class TestNetworkd(TestBase, _CommonTests): backend = 'networkd' def test_basic_missing_peer(self): ''' When networkd is the renderer, both peers are required ''' out = self.generate('''network: version: 2 virtual-ethernets: veth0: peer: veth1''', expect_fail=True) self.assertIn('veth0: interface \'veth1\' is not defined', out) def test_veth_peer_of_a_peer_was_not_defined(self): out = self.generate('''network: version: 2 renderer: networkd virtual-ethernets: veth0: peer: veth1 veth1: peer: abc''', expect_fail=True) self.assertIn('veth1: interface \'abc\' is not defined', out) netplan-1.0/tests/generator/test_vlans.py000066400000000000000000000205451457004145200207070ustar00rootroot00000000000000# # Tests for VLAN devices config generated via netplan # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import re import unittest from .base import TestBase, ND_VLAN, ND_EMPTY, ND_WITHIP, ND_DHCP6_WOCARRIER, \ NM_MANAGED, NM_UNMANAGED, NM_MANAGED_MAC, NM_UNMANAGED_MAC class TestNetworkd(TestBase): @unittest.skipIf("CODECOV_TOKEN" in os.environ, "Skipping on codecov.io: GLib changed hashtable elements order") def test_vlan(self): # pragma: nocover self.generate('''network: version: 2 ethernets: en1: {} vlans: enblue: id: 1 link: en1 addresses: [1.2.3.4/24] enred: id: 3 link: en1 macaddress: aa:bb:cc:dd:ee:11 engreen: {id: 2, link: en1, dhcp6: true}''') self.assert_networkd({'en1.network': '''[Match] Name=en1 [Network] LinkLocalAddressing=ipv6 VLAN=enblue VLAN=enred VLAN=engreen ''', 'enblue.netdev': ND_VLAN % ('enblue', 1), 'engreen.netdev': ND_VLAN % ('engreen', 2), 'enred.netdev': '''[NetDev] Name=enred MACAddress=aa:bb:cc:dd:ee:11 Kind=vlan [VLAN] Id=3 ''', 'enblue.network': ND_WITHIP % ('enblue', '1.2.3.4/24'), 'enred.network': (ND_EMPTY % ('enred', 'ipv6')) .replace('[Network]', '[Link]\nMACAddress=aa:bb:cc:dd:ee:11\n\n[Network]'), 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'en1' + NM_UNMANAGED % 'enblue' + NM_UNMANAGED % 'enred' + NM_UNMANAGED_MAC % 'aa:bb:cc:dd:ee:11' + NM_UNMANAGED % 'engreen') def test_vlan_sriov(self): # we need to make sure renderer: sriov vlans are not saved as part of # the NM/networkd config self.generate('''network: version: 2 ethernets: en1: {} vlans: enblue: id: 1 link: en1 renderer: sriov engreen: {id: 2, link: en1, dhcp6: true}''') self.assert_networkd({'en1.network': '''[Match] Name=en1 [Network] LinkLocalAddressing=ipv6 VLAN=engreen ''', 'engreen.netdev': ND_VLAN % ('engreen', 2), 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'en1' + NM_UNMANAGED % 'enblue' + NM_UNMANAGED % 'engreen') # see LP: #1888726 def test_vlan_parent_match(self): self.generate('''network: version: 2 renderer: networkd ethernets: lan: match: {macaddress: "11:22:33:44:55:66"} set-name: lan mtu: 9000 vlans: vlan20: {id: 20, link: lan}''') self.assert_networkd({'lan.network': '''[Match] PermanentMACAddress=11:22:33:44:55:66 Name=lan [Link] MTUBytes=9000 [Network] LinkLocalAddressing=ipv6 VLAN=vlan20 ''', 'lan.link': '''[Match] PermanentMACAddress=11:22:33:44:55:66 [Link] Name=lan WakeOnLan=off MTUBytes=9000 ''', 'vlan20.network': ND_EMPTY % ('vlan20', 'ipv6'), 'vlan20.netdev': ND_VLAN % ('vlan20', 20)}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'lan' + NM_UNMANAGED_MAC % '11:22:33:44:55:66' + NM_UNMANAGED % 'vlan20') def test_vlan_parent_must_exist(self): out = self.generate('''network: version: 2 renderer: networkd vlans: vlan20: {id: 20, link: lan}''', expect_fail=True) self.assertIn('vlan20: interface \'lan\' is not defined', out) class TestNetworkManager(TestBase): def test_vlan(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: en1: {} vlans: enblue: id: 1 link: en1 addresses: [1.2.3.4/24] engreen: {id: 2, link: en1, dhcp6: true}''') self.assert_networkd({}) self.assert_nm({'en1': '''[connection] id=netplan-en1 type=ethernet interface-name=en1 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'enblue': '''[connection] id=netplan-enblue type=vlan interface-name=enblue [vlan] id=1 parent=en1 [ipv4] method=manual address1=1.2.3.4/24 [ipv6] method=ignore ''', 'engreen': '''[connection] id=netplan-engreen type=vlan interface-name=engreen [vlan] id=2 parent=en1 [ipv4] method=link-local [ipv6] method=auto ip6-privacy=0 '''}) self.assert_nm_udev(NM_MANAGED % 'en1' + NM_MANAGED % 'enblue' + NM_MANAGED % 'engreen') def test_vlan_parent_match(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: en-v: match: {macaddress: "11:22:33:44:55:66"} vlans: engreen: {id: 2, link: en-v, dhcp4: true}''') self.assert_networkd({}) # get assigned UUID from en-v connection with open(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/netplan-en-v.nmconnection')) as f: m = re.search('uuid=([0-9a-fA-F-]{36})\n', f.read()) self.assertTrue(m) uuid = m.group(1) self.assertNotEqual(uuid, "00000000-0000-0000-0000-000000000000") self.assert_nm({'en-v': '''[connection] id=netplan-en-v type=ethernet uuid=%s [ethernet] wake-on-lan=0 mac-address=11:22:33:44:55:66 [ipv4] method=link-local [ipv6] method=ignore ''' % uuid, 'engreen': '''[connection] id=netplan-engreen type=vlan interface-name=engreen [vlan] id=2 parent=%s [ipv4] method=auto [ipv6] method=ignore ''' % uuid}) self.assert_nm_udev(NM_MANAGED_MAC % '11:22:33:44:55:66' + NM_MANAGED % 'engreen') def test_vlan_sriov(self): # we need to make sure renderer: sriov vlans are not saved as part of # the NM/networkd config self.generate('''network: version: 2 renderer: NetworkManager ethernets: en1: {} vlans: enblue: id: 1 link: en1 addresses: [1.2.3.4/24] renderer: sriov engreen: {id: 2, link: en1, dhcp6: true}''') self.assert_networkd({}) self.assert_nm({'en1': '''[connection] id=netplan-en1 type=ethernet interface-name=en1 [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore ''', 'engreen': '''[connection] id=netplan-engreen type=vlan interface-name=engreen [vlan] id=2 parent=en1 [ipv4] method=link-local [ipv6] method=auto ip6-privacy=0 '''}) self.assert_nm_udev(NM_MANAGED % 'en1' + NM_MANAGED % 'enblue' + NM_MANAGED % 'engreen') def test_vlan_parent_is_allowed_to_be_missing_for_nm(self): self.generate('''network: version: 2 renderer: NetworkManager vlans: vlan20: {id: 20, link: lan}''') self.assert_nm({'vlan20': '''[connection] id=netplan-vlan20 type=vlan interface-name=vlan20 [vlan] id=20 parent=lan [ipv4] method=link-local [ipv6] method=ignore '''}) def test_vlan_with_missing_netdef_found_in_the_next_file_wont_fail(self): ''' If eth0 was registered as missing (and created as a placeholder netdef) if shouldn't fail if it's defined in a file that was parsed *after* the file where the VLAN is defined ''' out = self.generate('''network: renderer: NetworkManager version: 2 vlans: vlan100: id: 100 link: eth0''', confs={'b': '''network: renderer: NetworkManager version: 2 ethernets: {eth0: {}}'''}) self.assertNotIn('Updated definition \'eth0\' changes device type', out) def test_vlan_with_parent_uuid(self): ''' Check the generate accepts a link pointing to the NM connection UUID ''' self.generate('''network: renderer: NetworkManager version: 2 vlans: vlan100: id: 100 link: 53125f52-f9a7-4d2a-a853-5f3b9e02299f''') netplan-1.0/tests/generator/test_vrfs.py000066400000000000000000000117621457004145200205450ustar00rootroot00000000000000# # Tests for bridge devices config generated via netplan # # Copyright (C) 2018 Canonical, Ltd. # Copyright (C) 2022 Datto, Inc. # Author: Anthony Timmins # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from .base import TestBase, ND_EMPTY, ND_DHCP, ND_VRF class NetworkManager(TestBase): def test_vrf_set_table(self): self.generate('''network: version: 2 renderer: NetworkManager ethernets: eth0: { dhcp4: true } vrfs: vrf1005: table: 1005 interfaces: [eth0] routes: - to: default via: 1.2.3.4 routing-policy: - from: 2.3.4.5''') self.assert_nm({'eth0': '''[connection] id=netplan-eth0 type=ethernet interface-name=eth0 slave-type=vrf # wokeignore:rule=slave master=vrf1005 # wokeignore:rule=master [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] method=ignore ''', 'vrf1005': '''[connection] id=netplan-vrf1005 type=vrf interface-name=vrf1005 [vrf] table=1005 [ipv4] route1=0.0.0.0/0,1.2.3.4 route1_options=table=1005 method=link-local [ipv6] method=ignore '''}) class TestNetworkd(TestBase): def test_vrf_set_table(self): self.generate('''network: version: 2 ethernets: eth0: { dhcp4: true } vrfs: vrf1005: table: 1005 interfaces: [eth0] routes: - to: default via: 1.2.3.4 routing-policy: - from: 2.3.4.5''') self.assert_networkd({'eth0.network': ND_DHCP % ('eth0', 'ipv4', '\nVRF=vrf1005', 'true'), 'vrf1005.network': ND_EMPTY % ('vrf1005', 'ipv6') + ''' [Route] Destination=0.0.0.0/0 Gateway=1.2.3.4 Table=1005 [RoutingPolicyRule] From=2.3.4.5 Table=1005 ''', 'vrf1005.netdev': ND_VRF % ('vrf1005', 1005)}) class TestNetplanYAMLv2(TestBase): '''No asserts are needed. The generate() method implicitly checks the (re-)generated YAML. ''' def test_vrf_table(self): self.generate('''network: version: 2 vrfs: vrf1005: table: 1005''') def test_vrf_routes(self): self.generate('''network: version: 2 vrfs: vrf1005: table: 1005 routes: - to: default via: 1.2.3.4 routing-policy: - from: 1.2.3.4''') class TestConfigErrors(TestBase): def test_vrf_missing_table(self): err = self.generate('''network: version: 2 vrfs: vrf1005: {}''', expect_fail=True) self.assertIn("vrf1005: missing 'table' property", err) def test_vrf_already_assigned(self): err = self.generate('''network: version: 2 vrfs: vrf0: table: 42 interfaces: [eno1] vrf1: table: 43 interfaces: [eno1] ethernets: eno1: {}''', expect_fail=True) self.assertIn("vrf1: interface 'eno1' is already assigned to vrf vrf0", err) def test_vrf_routes_table_mismatch(self): err = self.generate('''network: version: 2 vrfs: vrf0: table: 42 routes: - table: 42 # pass to: default via: 1.2.3.4 - table: 43 # mismatch to: 99.88.77.66 via: 2.3.4.5 ''', expect_fail=True) self.assertIn("vrf0: VRF routes table mismatch (42 != 43)", err) def test_vrf_policy_table_mismatch(self): err = self.generate('''network: version: 2 vrfs: vrf0: table: 45 routes: - to: default via: 3.4.5.6 routing-policy: - table: 45 # pass from: 1.2.3.4 - table: 46 # mismatch from: 2.3.4.5 ''', expect_fail=True) self.assertIn("vrf0: VRF routing-policy table mismatch (45 != 46)", err) def test_vrf_without_routing_policies_lp2016427(self): ''' Test that a VRF without policies will not crash. See LP: #2016427 ''' self.generate('''network: version: 2 vrfs: vrf1005: table: 1005 routes: - to: default via: 1.2.3.4''') self.assert_networkd({'vrf1005.network': ND_EMPTY % ('vrf1005', 'ipv6') + ''' [Route] Destination=0.0.0.0/0 Gateway=1.2.3.4 Table=1005 ''', 'vrf1005.netdev': ND_VRF % ('vrf1005', 1005)}) def test_vrf_without_routes(self): self.generate('''network: version: 2 vrfs: vrf1005: table: 1005 routing-policy: - from: 2.3.4.5''') self.assert_networkd({'vrf1005.network': ND_EMPTY % ('vrf1005', 'ipv6') + ''' [RoutingPolicyRule] From=2.3.4.5 Table=1005 ''', 'vrf1005.netdev': ND_VRF % ('vrf1005', 1005)}) netplan-1.0/tests/generator/test_wifis.py000066400000000000000000000666521457004145200207160ustar00rootroot00000000000000# # Tests for VLAN devices config generated via netplan # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import stat from .base import TestBase, ND_WIFI_DHCP4, SD_WPA, NM_MANAGED, NM_UNMANAGED class TestNetworkd(TestBase): def test_wifi(self): self.generate('''network: version: 2 wifis: wl0: regulatory-domain: "DE" access-points: "Joe's Home": password: "s0s3kr1t" bssid: 00:11:22:33:44:55 band: 2.4GHz channel: 11 workplace: password: "c0mpany1" bssid: de:ad:be:ef:ca:fe band: 5GHz channel: 100 peer2peer: mode: adhoc hidden-y: hidden: y password: "0bscur1ty" hidden-n: hidden: n password: "5ecur1ty" channel-no-band: channel: 7 band-no-channel: band: 2.4G band-no-channel2: band: 5G dhcp4: yes''') self.assert_networkd({'wl0.network': ND_WIFI_DHCP4 % 'wl0'}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'wl0') # generates wpa config and enables wpasupplicant unit with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: new_config = f.read() network = 'ssid="{}"\n freq_list='.format('band-no-channel2') freqs_5GHz = [5610, 5310, 5620, 5320, 5630, 5640, 5340, 5035, 5040, 5045, 5055, 5060, 5660, 5680, 5670, 5080, 5690, 5700, 5710, 5720, 5825, 5745, 5755, 5805, 5765, 5160, 5775, 5170, 5480, 5180, 5795, 5190, 5500, 5200, 5510, 5210, 5520, 5220, 5530, 5230, 5540, 5240, 5550, 5250, 5560, 5260, 5570, 5270, 5580, 5280, 5590, 5290, 5600, 5300, 5865, 5845, 5785] freqs = new_config.split(network) freqs = freqs[1].split('\n')[0] self.assertEqual(len(freqs.split(' ')), len(freqs_5GHz)) for freq in freqs_5GHz: self.assertRegex(new_config, '{}[ 0-9]*{}[ 0-9]*\n'.format(network, freq)) network = 'ssid="{}"\n freq_list='.format('band-no-channel') freqs_24GHz = [2412, 2417, 2422, 2427, 2432, 2442, 2447, 2437, 2452, 2457, 2462, 2467, 2472, 2484] freqs = new_config.split(network) freqs = freqs[1].split('\n')[0] self.assertEqual(len(freqs.split(' ')), len(freqs_24GHz)) for freq in freqs_24GHz: self.assertRegex(new_config, '{}[ 0-9]*{}[ 0-9]*\n'.format(network, freq)) self.assertIn(''' network={ ssid="channel-no-band" key_mgmt=NONE } ''', new_config) self.assertIn(''' network={ ssid="peer2peer" mode=1 key_mgmt=NONE } ''', new_config) self.assertIn(''' network={ ssid="hidden-y" scan_ssid=1 key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="0bscur1ty" } ''', new_config) self.assertIn(''' network={ ssid="hidden-n" key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="5ecur1ty" } ''', new_config) self.assertIn(''' network={ ssid="workplace" bssid=de:ad:be:ef:ca:fe freq_list=5500 key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="c0mpany1" } ''', new_config) self.assertIn(''' network={ ssid="Joe's Home" bssid=00:11:22:33:44:55 freq_list=2462 key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="s0s3kr1t" } ''', new_config) self.assertIn('country=DE\n', new_config) self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) self.assertTrue(os.path.isfile(os.path.join( self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')) as f: self.assertEqual(f.read(), SD_WPA % {'iface': 'wl0', 'drivers': 'nl80211,wext'}) self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644) self.assertTrue(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) def test_wifi_upgrade(self): # pretend an old 'netplan-wpa@*.service' link still exists on an upgraded system os.makedirs(os.path.join(self.workdir.name, 'lib/systemd/system')) os.makedirs(os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants')) with open(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), 'w') as out: out.write('''[Unit] Description=WPA supplicant for netplan %I DefaultDependencies=no Requires=sys-subsystem-net-devices-%i.device After=sys-subsystem-net-devices-%i.device Before=network.target Wants=network.target [Service] Type=simple ExecStart=/sbin/wpa_supplicant -c /run/netplan/wpa-%I.conf -i%I''') os.symlink(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service')) # run generate, which should cleanup the old files/symlinks self.generate('''network: version: 2 wifis: wl0: access-points: "Joe's Home": password: "s0s3kr1t" dhcp4: yes''') # verify new files/links exist, while old have been removed self.assertTrue(os.path.isfile(os.path.join( self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) self.assertTrue(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) # old files/links self.assertTrue(os.path.isfile(os.path.join( self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'))) self.assertFalse(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service'))) # pretend another old systemd service file exists for wl1 os.symlink(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl1.service')) # run generate again, to verify the historical netplan-wpa@.service links and wl0 links are gone self.generate('''network: version: 2 wifis: wl1: access-points: "Other Home": password: "s0s3kr1t" dhcp4: yes''') # verify new files/links exist, while old have been removed self.assertTrue(os.path.isfile(os.path.join( self.workdir.name, 'run/systemd/system/netplan-wpa-wl1.service'))) self.assertTrue(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl1.service'))) # old files/links self.assertTrue(os.path.isfile(os.path.join( self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'))) self.assertFalse(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl1.service'))) self.assertFalse(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service'))) self.assertFalse(os.path.isfile(os.path.join( self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) self.assertFalse(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) def test_wifi_route(self): self.generate('''network: version: 2 wifis: wl0: access-points: workplace: password: "c0mpany1" dhcp4: yes routes: - to: 10.10.10.0/24 via: 8.8.8.8''') self.assert_networkd({'wl0.network': '''[Match] Name=wl0 [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 [Route] Destination=10.10.10.0/24 Gateway=8.8.8.8 [DHCP] RouteMetric=600 UseMTU=true '''}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'wl0') def test_wifi_match(self): err = self.generate('''network: version: 2 wifis: somewifi: match: driver: foo access-points: workplace: password: "c0mpany1" dhcp4: yes''', expect_fail=True) self.assertIn('networkd backend does not support wifi with match:', err) def test_wifi_ap(self): err = self.generate('''network: version: 2 wifis: wl0: access-points: workplace: password: "c0mpany1" mode: ap dhcp4: yes''', expect_fail=True) self.assertIn('wl0: workplace: networkd does not support this wifi mode', err) def test_wifi_wowlan(self): self.generate('''network: version: 2 wifis: wl0: wakeonwlan: - any - disconnect - magic_pkt - gtk_rekey_failure - eap_identity_req - four_way_handshake - rfkill_release access-points: homenet: {mode: infrastructure}''') self.assert_networkd({'wl0.network': '''[Match] Name=wl0 [Network] LinkLocalAddressing=ipv6 '''}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'wl0') # generates wpa config and enables wpasupplicant unit with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: new_config = f.read() self.assertIn(''' wowlan_triggers=any disconnect magic_pkt gtk_rekey_failure eap_identity_req four_way_handshake rfkill_release network={ ssid="homenet" key_mgmt=NONE } ''', new_config) self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) self.assertTrue(os.path.isfile(os.path.join( self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) self.assertTrue(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) def test_wifi_wowlan_default(self): self.generate('''network: version: 2 wifis: wl0: wakeonwlan: [default] access-points: homenet: {mode: infrastructure}''') self.assert_networkd({'wl0.network': '''[Match] Name=wl0 [Network] LinkLocalAddressing=ipv6 '''}) self.assert_nm(None) self.assert_nm_udev(NM_UNMANAGED % 'wl0') # generates wpa config and enables wpasupplicant unit with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: new_config = f.read() self.assertIn(''' network={ ssid="homenet" key_mgmt=NONE } ''', new_config) self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) self.assertTrue(os.path.isfile(os.path.join( self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) self.assertTrue(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) def test_wifi_wpa3_personal(self): self.generate('''network: version: 2 wifis: wl0: access-points: homenet: auth: key-management: sae password: "********"''') self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant network={ ssid="homenet" key_mgmt=SAE ieee80211w=2 psk="********" } """) def test_wifi_wpa3_enterprise_eap_sha256(self): self.generate('''network: version: 2 wifis: wl0: access-points: homenet: auth: key-management: eap-sha256 method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "**********"''') self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant network={ ssid="homenet" key_mgmt=WPA-EAP WPA-EAP-SHA256 eap=TLS ieee80211w=1 identity="cert-joe@cust.example.com" anonymous_identity="@cust.example.com" ca_cert="/etc/ssl/cust-cacrt.pem" client_cert="/etc/ssl/cust-crt.pem" private_key="/etc/ssl/cust-key.pem" private_key_passwd="**********" } """) def test_wifi_wpa3_enterprise_eap_suite_b_192(self): self.generate('''network: version: 2 wifis: wl0: access-points: homenet: auth: key-management: eap-suite-b-192 method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "**********"''') self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant network={ ssid="homenet" key_mgmt=WPA-EAP-SUITE-B-192 eap=TLS ieee80211w=2 identity="cert-joe@cust.example.com" anonymous_identity="@cust.example.com" ca_cert="/etc/ssl/cust-cacrt.pem" client_cert="/etc/ssl/cust-crt.pem" private_key="/etc/ssl/cust-key.pem" private_key_passwd="**********" } """) def test_wifi_ieee8021x_eap_leap(self): self.generate('''network: version: 2 wifis: wl0: access-points: homenet: auth: key-management: 802.1x method: leap identity: some-id password: "********"''') self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant network={ ssid="homenet" key_mgmt=IEEE8021X eap=LEAP identity="some-id" password="********" } """) def test_wifi_ieee8021x_eap_pwd(self): self.generate('''network: version: 2 wifis: wl0: access-points: homenet: auth: key-management: 802.1x method: pwd identity: some-id password: "********"''') self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant network={ ssid="homenet" key_mgmt=IEEE8021X eap=PWD identity="some-id" password="********" } """) def test_wifi_ieee8021x_eap_and_psk(self): self.generate('''network: version: 2 wifis: wl0: access-points: homenet: password: psk_password auth: key-management: eap method: leap identity: some-id password: "********"''') self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant network={ ssid="homenet" key_mgmt=WPA-EAP eap=LEAP ieee80211w=1 identity="some-id" psk="psk_password" password="********" } """) class TestNetworkManager(TestBase): def test_wifi_default(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: "Joe's Home": password: "s0s3kr1t" bssid: 00:11:22:33:44:55 band: 2.4GHz channel: 11 workplace: password: "c0mpany1" bssid: de:ad:be:ef:ca:fe band: 5GHz channel: 100 hidden-y: hidden: y password: "0bscur1ty" hidden-n: hidden: n password: "5ecur1ty" channel-no-band: channel: 22 band-no-channel: band: 5GHz dhcp4: yes''') self.assert_nm({'wl0-Joe%27s%20Home': '''[connection] id=netplan-wl0-Joe's Home type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=Joe's Home mode=infrastructure bssid=00:11:22:33:44:55 band=bg channel=11 [wifi-security] key-mgmt=wpa-psk pmf=2 psk=s0s3kr1t ''', 'wl0-workplace': '''[connection] id=netplan-wl0-workplace type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=workplace mode=infrastructure bssid=de:ad:be:ef:ca:fe band=a channel=100 [wifi-security] key-mgmt=wpa-psk pmf=2 psk=c0mpany1 ''', 'wl0-hidden-y': '''[connection] id=netplan-wl0-hidden-y type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=hidden-y mode=infrastructure hidden=true [wifi-security] key-mgmt=wpa-psk pmf=2 psk=0bscur1ty ''', 'wl0-hidden-n': '''[connection] id=netplan-wl0-hidden-n type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=hidden-n mode=infrastructure [wifi-security] key-mgmt=wpa-psk pmf=2 psk=5ecur1ty ''', 'wl0-channel-no-band': '''[connection] id=netplan-wl0-channel-no-band type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=channel-no-band mode=infrastructure ''', 'wl0-band-no-channel': '''[connection] id=netplan-wl0-band-no-channel type=wifi interface-name=wl0 [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=band-no-channel mode=infrastructure band=a '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'wl0') def test_wifi_match_mac(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: all: match: macaddress: 11:22:33:44:55:66 access-points: workplace: {}''') self.assert_nm({'all-workplace': '''[connection] id=netplan-all-workplace type=wifi [wifi] mac-address=11:22:33:44:55:66 ssid=workplace mode=infrastructure [ipv4] method=link-local [ipv6] method=ignore '''}) def test_wifi_match_all(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: all: match: {} access-points: workplace: {mode: infrastructure}''') self.assert_nm({'all-workplace': '''[connection] id=netplan-all-workplace type=wifi [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=workplace mode=infrastructure '''}, '''[device-netplan.wifis.all] match-device=type:wifi managed=1\n\n''') def test_wifi_ap(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: homenet: mode: ap password: s0s3cret''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [ipv4] method=shared [ipv6] method=ignore [wifi] ssid=homenet mode=ap [wifi-security] key-mgmt=wpa-psk pmf=2 psk=s0s3cret '''}) self.assert_networkd({}) self.assert_nm_udev(NM_MANAGED % 'wl0') def test_wifi_adhoc(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: homenet: mode: adhoc''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=homenet mode=adhoc '''}) def test_wifi_adhoc_wpa_24ghz(self): self.generate('''network: version: 2 wifis: wl0: access-points: homenet: mode: adhoc band: 2.4GHz channel: 7 password: "********"''') self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant network={ ssid="homenet" frequency=2442 mode=1 key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="********" } """) def test_wifi_adhoc_wpa_5ghz(self): self.generate('''network: version: 2 wifis: wl0: access-points: homenet: mode: adhoc band: 5GHz channel: 7 password: "********"''') self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant network={ ssid="homenet" frequency=5035 mode=1 key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE ieee80211w=1 psk="********" } """) def test_wifi_wpa3_personal(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: homenet: auth: key-management: sae password: "********"''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=homenet mode=infrastructure [wifi-security] key-mgmt=sae pmf=3 psk=******** '''}) def test_wifi_wpa3_enterprise_eap_sha256(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: homenet: auth: key-management: eap-sha256 method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "**********"''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=homenet mode=infrastructure [wifi-security] key-mgmt=wpa-eap pmf=2 [802-1x] eap=tls identity=cert-joe@cust.example.com anonymous-identity=@cust.example.com ca-cert=/etc/ssl/cust-cacrt.pem client-cert=/etc/ssl/cust-crt.pem private-key=/etc/ssl/cust-key.pem private-key-password=********** '''}) def test_wifi_wpa3_enterprise_eap_suite_b_192(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: homenet: auth: key-management: eap-suite-b-192 method: tls anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: /etc/ssl/cust-cacrt.pem client-certificate: /etc/ssl/cust-crt.pem client-key: /etc/ssl/cust-key.pem client-key-password: "**********"''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=homenet mode=infrastructure [wifi-security] key-mgmt=wpa-eap-suite-b-192 pmf=3 [802-1x] eap=tls identity=cert-joe@cust.example.com anonymous-identity=@cust.example.com ca-cert=/etc/ssl/cust-cacrt.pem client-cert=/etc/ssl/cust-crt.pem private-key=/etc/ssl/cust-key.pem private-key-password=********** '''}) def test_wifi_ieee8021x_leap(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: homenet: auth: key-management: 802.1x method: leap identity: "some-id" password: "**********"''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=homenet mode=infrastructure [wifi-security] key-mgmt=ieee8021x [802-1x] eap=leap identity=some-id password=********** '''}) def test_wifi_ieee8021x_pwd(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: homenet: auth: key-management: 802.1x method: pwd identity: "some-id" password: "**********"''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=homenet mode=infrastructure [wifi-security] key-mgmt=ieee8021x [802-1x] eap=pwd identity=some-id password=********** '''}) def test_wifi_eap_and_psk(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: access-points: homenet: password: psk_password auth: key-management: eap method: leap identity: "some-id" password: "**********"''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=homenet mode=infrastructure [wifi-security] key-mgmt=wpa-eap pmf=2 psk=psk_password [802-1x] eap=leap identity=some-id password=********** '''}) def test_wifi_wowlan(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: wakeonwlan: [any, tcp, four_way_handshake, magic_pkt] access-points: homenet: {mode: infrastructure}''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [wifi] wake-on-wlan=330 ssid=homenet mode=infrastructure [ipv4] method=link-local [ipv6] method=ignore '''}) def test_wifi_wowlan_default(self): self.generate('''network: version: 2 renderer: NetworkManager wifis: wl0: wakeonwlan: [default] access-points: homenet: {mode: infrastructure}''') self.assert_nm({'wl0-homenet': '''[connection] id=netplan-wl0-homenet type=wifi interface-name=wl0 [ipv4] method=link-local [ipv6] method=ignore [wifi] ssid=homenet mode=infrastructure '''}) def test_wifi_regdom(self): out = self.generate('''network: wifis: wl0: regulatory-domain: GB access-points: homenet: {mode: infrastructure} wl1: regulatory-domain: DE access-points: homenet2: {mode: infrastructure}''') self.assertIn('wl1: Conflicting regulatory-domain (GB vs DE)', out) with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: new_config = f.read() self.assertIn('country=GB\n', new_config) with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl1.conf')) as f: new_config = f.read() self.assertIn('country=DE\n', new_config) with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-regdom.service')) as f: new_config = f.read() self.assertIn('ExecStart=/usr/sbin/iw reg set DE\n', new_config) class TestConfigErrors(TestBase): def test_wifi_invalid_wowlan(self): err = self.generate('''network: version: 2 wifis: wl0: wakeonwlan: [bogus] access-points: homenet: {mode: infrastructure}''', expect_fail=True) self.assertIn("Error in network definition: invalid value for wakeonwlan: 'bogus'", err) def test_wifi_wowlan_unsupported(self): err = self.generate('''network: version: 2 wifis: wl0: wakeonwlan: [tcp] access-points: homenet: {mode: infrastructure}''', expect_fail=True) self.assertIn("ERROR: unsupported wowlan_triggers mask: 0x100", err) def test_wifi_wowlan_exclusive(self): err = self.generate('''network: version: 2 wifis: wl0: wakeonwlan: [default, magic_pkt] access-points: homenet: {mode: infrastructure}''', expect_fail=True) self.assertIn("Error in network definition: 'default' is an exclusive flag for wakeonwlan", err) netplan-1.0/tests/integration/000077500000000000000000000000001457004145200165025ustar00rootroot00000000000000netplan-1.0/tests/integration/__init__.py000066400000000000000000000013051457004145200206120ustar00rootroot00000000000000# # Integration tests. # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . netplan-1.0/tests/integration/base.py000066400000000000000000000553431457004145200200000ustar00rootroot00000000000000# # System integration tests of netplan-generate. NM and networkd are # started on the generated configuration, using emulated ethernets (veth) and # Wifi (mac80211-hwsim). These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2023 Canonical, Ltd. # Author: Martin Pitt # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys import re import time import subprocess import tempfile import unittest import shutil import gi import glob import json # make sure we point to libnetplan properly. os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))}) test_backends = "networkd NetworkManager" if "NETPLAN_TEST_BACKENDS" not in os.environ else os.environ["NETPLAN_TEST_BACKENDS"] for program in ['wpa_supplicant', 'hostapd', 'dnsmasq']: if subprocess.call(['which', program], stdout=subprocess.PIPE) != 0: sys.stderr.write('%s is required for this test suite, but not available. Skipping\n' % program) sys.exit(0) nm_uses_dnsmasq = b'dns=dnsmasq' in subprocess.check_output(['NetworkManager', '--print-config']) def resolved_in_use(): return os.path.isfile('/run/systemd/resolve/resolv.conf') class IntegrationTestsBase(unittest.TestCase): '''Common functionality for network test cases setUp() creates two test ethernet devices (self.dev_e_{ap,client} and self.dev_e2_{ap,client}. Each test should call self.setup_eth() with the desired configuration. ''' @classmethod def setUpClass(klass): shutil.rmtree('/etc/netplan', ignore_errors=True) os.makedirs('/etc/netplan', exist_ok=True) # Try to keep autopkgtest's management network (eth0/ens3) up and # configured. It should be running all the time, independently of netplan os.makedirs('/etc/systemd/network', exist_ok=True) with open('/etc/systemd/network/20-wired.network', 'w') as f: f.write('[Match]\nName=eth0 en*\n\n[Network]\nDHCP=yes\nKeepConfiguration=yes') # force-reset NM's unmanaged-devices list (using "=" instead of "+=") # from /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf, # to stop it from trampling over our test & mgmt interfaces. # https://pad.lv/1615044 os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) with open('/etc/NetworkManager/conf.d/90-test-ignore.conf', 'w') as f: f.write('[keyfile]\nunmanaged-devices=interface-name:en*,eth0,nptestsrv') subprocess.check_call(['netplan', 'apply']) subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) if klass.is_active('NetworkManager.service'): subprocess.check_call(['nm-online', '-s']) subprocess.check_call(['nmcli', 'general', 'reload']) @classmethod def tearDownClass(klass): pass def tearDown(self): subprocess.call(['systemctl', 'stop', 'NetworkManager', 'systemd-networkd', 'netplan-wpa-*', 'netplan-ovs-*', 'systemd-networkd.socket']) # NM has KillMode=process and leaks dhclient processes subprocess.call(['systemctl', 'kill', 'NetworkManager']) subprocess.call(['systemctl', 'reset-failed', 'NetworkManager', 'systemd-networkd'], stderr=subprocess.DEVNULL) shutil.rmtree('/etc/netplan', ignore_errors=True) shutil.rmtree('/run/NetworkManager', ignore_errors=True) shutil.rmtree('/run/systemd/network', ignore_errors=True) for f in glob.glob('/run/systemd/system/netplan-*'): os.remove(f) for f in glob.glob('/run/systemd/system/**/netplan-*'): os.remove(f) for f in glob.glob('/run/udev/rules.d/*netplan*'): os.remove(f) subprocess.call(['systemctl', 'daemon-reload']) subprocess.call(['udevadm', 'control', '--reload']) subprocess.call(['udevadm', 'trigger', '--attr-match=subsystem=net']) subprocess.call(['udevadm', 'settle']) try: os.remove('/run/systemd/generator/netplan.stamp') except FileNotFoundError: pass # Keep the management network (eth0/ens3 from 20-wired.network) up subprocess.check_call(['systemctl', 'restart', 'systemd-networkd']) @classmethod def create_devices(klass): '''Create Access Point and Client devices with veth''' if os.path.exists('/sys/class/net/eth42'): raise SystemError('eth42 interface already exists') klass.dev_e_ap = 'veth42' klass.dev_e_client = 'eth42' klass.dev_e_ap_ip4 = '192.168.5.1/24' klass.dev_e_ap_ip6 = '2600::1/64' klass.dev_e2_ap = 'veth43' klass.dev_e2_client = 'eth43' klass.dev_e2_ap_ip4 = '192.168.6.1/24' klass.dev_e2_ap_ip6 = '2601::1/64' # don't let NM trample over our test routers with open('/etc/NetworkManager/conf.d/99-test-denylist.conf', 'w') as f: f.write('[keyfile]\nunmanaged-devices+=%s,%s\n' % (klass.dev_e_ap, klass.dev_e2_ap)) if klass.is_active('NetworkManager.service'): subprocess.check_call(['nm-online', '-s']) subprocess.check_call(['nmcli', 'general', 'reload']) # create virtual ethernet devs subprocess.check_call(['ip', 'link', 'add', 'name', 'eth42', 'type', 'veth', 'peer', 'name', 'veth42']) subprocess.check_call(['ip', 'link', 'add', 'name', 'eth43', 'type', 'veth', 'peer', 'name', 'veth43']) # Creation of the veths introduces a race with newer versions of # systemd, as it will change the initial MAC address after the device # was created and networkd took control. Give it some time, so we read # the correct MAC address time.sleep(0.1) out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth42'], text=True) klass.dev_e_client_mac = out.split()[2] out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth43'], text=True) klass.dev_e2_client_mac = out.split()[2] @classmethod def shutdown_devices(klass): '''Remove test devices''' subprocess.check_call(['ip', 'link', 'del', 'dev', klass.dev_e_ap]) subprocess.check_call(['ip', 'link', 'del', 'dev', klass.dev_e2_ap]) klass.dev_e_ap = None klass.dev_e_client = None klass.dev_e2_ap = None klass.dev_e2_client = None subprocess.call(['ip', 'link', 'del', 'dev', 'mybr'], stderr=subprocess.PIPE) subprocess.call(['ip', 'link', 'del', 'dev', 'nptestsrv'], stderr=subprocess.PIPE) os.remove('/etc/NetworkManager/conf.d/99-test-denylist.conf') def setUp(self): '''Create test devices and workdir''' self.create_devices() self.addCleanup(self.shutdown_devices) self.workdir_obj = tempfile.TemporaryDirectory() self.workdir = self.workdir_obj.name self.config = '/etc/netplan/01-main.yaml' os.makedirs('/etc/netplan', exist_ok=True) # create static entropy file to avoid draining/blocking on /dev/random self.entropy_file = os.path.join(self.workdir, 'entropy') with open(self.entropy_file, 'wb') as f: f.write(b'012345678901234567890') def setup_eth(self, ipv6_mode, start_dnsmasq=True): '''Set up simulated ethernet router On self.dev_e_ap, run dnsmasq according to ipv6_mode, see start_dnsmasq(). This is torn down automatically at the end of the test. ''' # give our router an IP subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_e_ap]) subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_e2_ap]) if ipv6_mode is not None: subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip6, 'dev', self.dev_e_ap]) subprocess.check_call(['ip', 'a', 'add', self.dev_e2_ap_ip6, 'dev', self.dev_e2_ap]) else: subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip4, 'dev', self.dev_e_ap]) subprocess.check_call(['ip', 'a', 'add', self.dev_e2_ap_ip4, 'dev', self.dev_e2_ap]) subprocess.check_call(['ip', 'link', 'set', self.dev_e_ap, 'up']) subprocess.check_call(['ip', 'link', 'set', self.dev_e2_ap, 'up']) if start_dnsmasq: self.start_dnsmasq(ipv6_mode, self.dev_e_ap) self.start_dnsmasq(ipv6_mode, self.dev_e2_ap) # # Internal implementation details # @classmethod def poll_text(klass, logpath, string, timeout=50): '''Poll log file for a given string with a timeout. Timeout is given in deciseconds. ''' log = '' while timeout > 0: if os.path.exists(logpath): break timeout -= 1 time.sleep(0.1) assert timeout > 0, 'Timed out waiting for file %s to appear' % logpath with open(logpath) as f: while timeout > 0: line = f.readline() if line: log += line if string in line: break continue timeout -= 1 time.sleep(0.1) assert timeout > 0, 'Timed out waiting for "%s":\n------------\n%s\n-------\n' % (string, log) def start_dnsmasq(self, ipv6_mode, iface): '''Start dnsmasq. If ipv6_mode is None, IPv4 is set up with DHCP. If it is not None, it must be a valid dnsmasq mode, i. e. a combination of "ra-only", "slaac", "ra-stateless", and "ra-names". See dnsmasq(8). ''' if ipv6_mode is None: if iface == self.dev_e2_ap: dhcp_range = '192.168.6.10,192.168.6.200' else: dhcp_range = '192.168.5.10,192.168.5.200' else: if iface == self.dev_e2_ap: dhcp_range = '2601::10,2601::20' else: dhcp_range = '2600::10,2600::20' if ipv6_mode: dhcp_range += ',' + ipv6_mode dnsmasq_log = os.path.join(self.workdir, 'dnsmasq-%s.log' % iface) lease_file = os.path.join(self.workdir, 'dnsmasq-%s.leases' % iface) p = subprocess.Popen(['dnsmasq', '--keep-in-foreground', '--log-queries', '--log-facility=' + dnsmasq_log, '--conf-file=/dev/null', '--dhcp-leasefile=' + lease_file, '--bind-interfaces', '--interface=' + iface, '--except-interface=lo', '--enable-ra', '--dhcp-range=' + dhcp_range]) self.addCleanup(p.kill) if ipv6_mode is not None: self.poll_text(dnsmasq_log, 'IPv6 router advertisement enabled') else: self.poll_text(dnsmasq_log, 'DHCP, IP range') def iface_json(self, iface: str) -> dict: '''Return iproute2's (detailed) JSON representation''' out = subprocess.check_output(['ip', '-j', '-d', 'a', 'show', 'dev', iface], text=True) json_dict = json.loads(out) if json_dict: return json_dict[0] return {} def assert_iface(self, iface, expected_ip_a=None, unexpected_ip_a=None): '''Assert that client interface has been created''' out = subprocess.check_output(['ip', '-d', 'a', 'show', 'dev', iface], text=True) if expected_ip_a: for r in expected_ip_a: self.assertRegex(out, r, out) if unexpected_ip_a: for r in unexpected_ip_a: self.assertNotRegex(out, r, out) return out def assert_iface_up(self, iface, expected_ip_a=None, unexpected_ip_a=None): '''Assert that client interface is up''' out = self.assert_iface(iface, expected_ip_a, unexpected_ip_a) if 'bond' not in iface: self.assertIn('state UP', out) def match_veth_by_non_permanent_mac_quirk(self, netdef_id, mac_match): ''' We cannot match using PermanentMACAddress= on veth type devices. Still, we need this capability during testing. So we're applying this quirk to install some override config snippet. https://github.com/canonical/netplan/pull/278 ''' network_dir = '10-netplan-' + netdef_id + '.network.d' link_dir = '10-netplan-' + netdef_id + '.link.d' for dir in [network_dir, link_dir]: path = os.path.join('/run/systemd/network', dir) os.makedirs(path, exist_ok=False) # cleanup is done in tearDown() with open(os.path.join(path, 'override.conf'), 'w') as f: # clear the PermanentMACAddress= setting and use MACAddress= instead f.write('[Match]\nPermanentMACAddress=\nMACAddress={}'.format(mac_match)) def generate_and_settle(self, wait_interfaces=None, state_dir=None): '''Generate config, launch and settle NM and networkd''' # regenerate netplan config cmd = ['netplan', 'apply'] if state_dir: cmd = cmd + ['--state', state_dir] out = '' try: out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True) except subprocess.CalledProcessError as e: self.assertTrue(False, 'netplan apply failed: {}'.format(e.output)) if 'Run \'systemctl daemon-reload\' to reload units.' in out: print('\nWARNING: systemd units changed without reload:', out) # start NM so that we can verify that it does not manage anything subprocess.call(['nm-online', '-sxq']) # Wait for NM startup, from 'netplan apply' if not self.is_active('NetworkManager.service'): subprocess.check_call(['systemctl', 'start', 'NetworkManager.service']) subprocess.call(['nm-online', '-sq']) # Debugging output # out = subprocess.check_output(['NetworkManager', '--print-config'], text=True) # print(out, flush=True) # out = subprocess.check_output(['nmcli', 'dev'], text=True) # print(out, flush=True) # Wait for interfaces to be ready: ifaces = wait_interfaces if wait_interfaces is not None else [self.dev_e_client, self.dev_e2_client] for iface_state in ifaces: split = iface_state.split('/', 1) iface = split[0] state = split[1] if len(split) > 1 else None print(iface, end=' ', flush=True) if self.backend == 'NetworkManager': self.nm_wait_connected(iface, 60) else: self.networkd_wait_connected(iface, 60) # wait for iproute2 state change if state: self.wait_output(['ip', 'addr', 'show', iface], state, 30) def state(self, iface, state): '''Tell generate_and_settle() to wait for a specific state''' return iface + '/' + state def state_up(self, iface): '''Tell generate_and_settle() to wait for the interface to be brought UP''' return self.state(iface, 'state UP') def state_dhcp4(self, iface): '''Tell generate_and_settle() to wait for assignment of an IP4 address from DHCP''' return self.state(iface, 'inet 192.168.') # TODO: make this a regex to check for specific DHCP ranges def state_dhcp6(self, iface): '''Tell generate_and_settle() to wait for assignment of an IP6 address from DHCP''' return self.state(iface, 'inet6 260') # TODO: make this a regex to check for specific DHCP ranges def nm_online_full(self, iface, timeout=60): '''Wait for NetworkManager connection to be completed (incl. IP4 & DHCP)''' gi.require_version('NM', '1.0') from gi.repository import NM for t in range(timeout): c = NM.Client.new(None) con = c.get_device_by_iface(iface).get_active_connection() if not con: self.fail('no active connection for %s by NM' % iface) flags = NM.utils_enum_to_str(NM.ActivationStateFlags, con.get_state_flags()) if "ip4-ready" in flags: break time.sleep(1) else: self.fail('timed out waiting for %s to get ready by NM' % iface) def wait_output(self, cmd, expected_output, timeout=10): for _ in range(timeout): try: out = subprocess.check_output(cmd, text=True) except subprocess.CalledProcessError: out = '' if expected_output in out: break sys.stdout.write('.') # waiting indicator sys.stdout.flush() time.sleep(1) else: subprocess.call(cmd) # print output of the failed command self.fail('timed out waiting for "{}" to appear in {}'.format(expected_output, cmd)) def nm_wait_connected(self, iface, timeout=10): self.wait_output(['nmcli', 'dev', 'show', iface], '(connected', timeout) def networkd_wait_connected(self, iface, timeout=10): # "State: routable (configured)" or "State: degraded (configured)" self.wait_output(['networkctl', 'status', iface], '(configured', timeout) @classmethod def is_active(klass, unit): '''Check if given unit is active or activating''' p = subprocess.Popen(['systemctl', 'is-active', unit], stdout=subprocess.PIPE) out = p.communicate()[0] return p.returncode == 0 or out.startswith(b'activating') class IntegrationTestsWifi(IntegrationTestsBase): '''Common functionality for network test cases setUp() creates two test wlan devices, one for a simulated access point (self.dev_w_ap), the other for a simulated client device (self.dev_w_client), and two test ethernet devices (self.dev_e_{ap,client} and self.dev_e2_{ap,client}. Each test should call self.setup_ap() or self.setup_eth() with the desired configuration. ''' @classmethod def setUpClass(klass): super().setUpClass() # ensure we have this so that iw works try: subprocess.check_call(['modprobe', 'cfg80211']) # set regulatory domain "EU", so that we can use 80211.a 5 GHz channels out = subprocess.check_output(['iw', 'reg', 'get'], text=True) m = re.match(r'^(?:global\n)?country (\S+):', out) assert m klass.orig_country = m.group(1) subprocess.check_call(['iw', 'reg', 'set', 'EU']) except Exception: raise unittest.SkipTest("cfg80211 (wireless) is unavailable, can't test") @classmethod def tearDownClass(klass): subprocess.check_call(['iw', 'reg', 'set', klass.orig_country]) super().tearDownClass() @classmethod def create_devices(klass): '''Create Access Point and Client devices with mac80211_hwsim and veth''' if os.path.exists('/sys/module/mac80211_hwsim'): raise SystemError('mac80211_hwsim module already loaded') super().create_devices() # create virtual wlan devs before_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) subprocess.check_call(['modprobe', 'mac80211_hwsim']) # wait 5 seconds for fake devices to appear timeout = 50 while timeout > 0: after_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) if len(after_wlan) - len(before_wlan) >= 2: break timeout -= 1 time.sleep(0.1) else: raise SystemError('timed out waiting for fake devices to appear') devs = list(after_wlan - before_wlan) klass.dev_w_ap = devs[0] klass.dev_w_client = devs[1] # don't let NM trample over our fake AP with open('/etc/NetworkManager/conf.d/99-test-denylist-wifi.conf', 'w') as f: f.write('[keyfile]\nunmanaged-devices+=%s\n' % klass.dev_w_ap) @classmethod def shutdown_devices(klass): '''Remove test devices''' super().shutdown_devices() klass.dev_w_ap = None klass.dev_w_client = None subprocess.check_call(['rmmod', 'mac80211_hwsim']) os.remove('/etc/NetworkManager/conf.d/99-test-denylist-wifi.conf') def start_hostapd(self, conf): hostapd_conf = os.path.join(self.workdir, 'hostapd.conf') with open(hostapd_conf, 'w') as f: f.write('interface=%s\ndriver=nl80211\n' % self.dev_w_ap) f.write(conf) log = os.path.join(self.workdir, 'hostapd.log') p = subprocess.Popen(['hostapd', '-e', self.entropy_file, '-f', log, hostapd_conf], stdout=subprocess.PIPE) self.addCleanup(p.wait) self.addCleanup(p.terminate) self.poll_text(log, '' + self.dev_w_ap + ': AP-ENABLED', 500) def setup_ap(self, hostapd_conf, ipv6_mode): '''Set up simulated access point On self.dev_w_ap, run hostapd with given configuration. Setup dnsmasq according to ipv6_mode, see start_dnsmasq(). This is torn down automatically at the end of the test. ''' # give our AP an IP subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_w_ap]) if ipv6_mode is not None: subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip6, 'dev', self.dev_w_ap]) else: subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip4, 'dev', self.dev_w_ap]) self.start_hostapd(hostapd_conf) self.start_dnsmasq(ipv6_mode, self.dev_w_ap) def assert_iface_up(self, iface, expected_ip_a=None, unexpected_ip_a=None): '''Assert that client interface is up''' super().assert_iface_up(iface, expected_ip_a, unexpected_ip_a) if iface == self.dev_w_client: out = subprocess.check_output(['iw', 'dev', iface, 'link'], text=True) # self.assertIn('Connected to ' + self.mac_w_ap, out) self.assertIn('SSID: fake net', out) netplan-1.0/tests/integration/bonds.py000066400000000000000000000701461457004145200201710ustar00rootroot00000000000000#!/usr/bin/python3 # # Integration tests for bonds # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2021 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def test_bond_base(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) def test_bond_primary_member(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: {} %(e2c)s: {} bonds: mybond: interfaces: [%(ec)s, %(e2c)s] parameters: mode: active-backup primary: %(ec)s addresses: [ '10.10.10.1/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'mybond']) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up(self.dev_e2_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 10.10.10.1/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave result = f.read().strip() self.assertIn(self.dev_e_client, result) self.assertIn(self.dev_e2_client, result) with open('/sys/class/net/mybond/bonding/primary') as f: self.assertEqual(f.read().strip(), '%(ec)s' % {'ec': self.dev_e_client}) def test_bond_all_members_active(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: all-members-active: true dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/all_slaves_active') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), '1') def test_bond_mode_8023ad(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: parameters: mode: 802.3ad interfaces: [ethbn] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/mode') as f: self.assertEqual(f.read().strip(), '802.3ad 4') def test_bond_mode_8023ad_adselect(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: parameters: mode: 802.3ad ad-select: bandwidth interfaces: [ethbn] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/ad_select') as f: self.assertEqual(f.read().strip(), 'bandwidth 1') def test_bond_mode_8023ad_lacp_rate(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: parameters: mode: 802.3ad lacp-rate: fast interfaces: [ethbn] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/lacp_rate') as f: self.assertEqual(f.read().strip(), 'fast 1') def test_bond_mode_activebackup_failover_mac(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: parameters: mode: active-backup fail-over-mac-policy: follow interfaces: [ethbn] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/mode') as f: self.assertEqual(f.read().strip(), 'active-backup 1') with open('/sys/class/net/mybond/bonding/fail_over_mac') as f: self.assertEqual(f.read().strip(), 'follow 2') def test_bond_mode_balance_xor(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: parameters: mode: balance-xor interfaces: [ethbn] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/mode') as f: self.assertEqual(f.read().strip(), 'balance-xor 2') def test_bond_mode_balance_rr(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: parameters: mode: balance-rr interfaces: [ethbn] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/mode') as f: self.assertEqual(f.read().strip(), 'balance-rr 0') def test_bond_mode_balance_rr_pps(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: parameters: mode: balance-rr packets-per-member: 15 interfaces: [ethbn] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/mode') as f: self.assertEqual(f.read().strip(), 'balance-rr 0') with open('/sys/class/net/mybond/bonding/packets_per_slave') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), '15') def test_bond_resend_igmp(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} ethb2: match: {name: %(e2c)s} bonds: mybond: addresses: [192.168.9.9/24] interfaces: [ethbn, ethb2] parameters: mode: balance-rr mii-monitor-interval: 50s resend-igmp: 100 ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'mybond']) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up(self.dev_e2_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.9.9/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave result = f.read().strip() self.assertIn(self.dev_e_client, result) self.assertIn(self.dev_e2_client, result) with open('/sys/class/net/mybond/bonding/resend_igmp') as f: self.assertEqual(f.read().strip(), '100') @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' def test_bond_mac(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: name: %(ec)s bonds: mybond: interfaces: [ethbn] macaddress: 00:01:02:03:04:05 dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24', '00:01:02:03:04:05']) def test_bond_down_delay(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: active-backup mii-monitor-interval: 5 down-delay: 10s dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/downdelay') as f: self.assertEqual(f.read().strip(), '10000') def test_bond_up_delay(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: active-backup mii-monitor-interval: 5 up-delay: 10000 dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/updelay') as f: self.assertEqual(f.read().strip(), '10000') def test_bond_arp_interval(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: balance-xor arp-ip-targets: [ 192.168.5.1 ] arp-interval: 50s dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/arp_interval') as f: self.assertEqual(f.read().strip(), '50000') def test_bond_arp_targets(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: balance-xor arp-interval: 50000 arp-ip-targets: [ 192.168.5.1 ] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: self.assertEqual(f.read().strip(), '192.168.5.1') def test_bond_arp_targets_many_lp1829264(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: balance-xor arp-interval: 50000 arp-ip-targets: [ 192.168.5.1, 192.168.5.34 ] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: result = f.read().strip() self.assertIn('192.168.5.1', result) self.assertIn('192.168.5.34', result) def test_bond_arp_all_targets(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: balance-xor arp-ip-targets: [192.168.5.1] arp-interval: 50000 arp-all-targets: all arp-validate: all dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/arp_all_targets') as f: self.assertEqual(f.read().strip(), 'all 1') def test_bond_arp_validate(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: balance-xor arp-ip-targets: [192.168.5.1] arp-interval: 50000 arp-validate: all dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/arp_validate') as f: self.assertEqual(f.read().strip(), 'all 3') @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' @unittest.skip("NetworkManager does not support setting MAC for a bond") def test_bond_mac(self): pass def test_bond_down_delay(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: active-backup mii-monitor-interval: 5 down-delay: 10000 dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/downdelay') as f: self.assertEqual(f.read().strip(), '10000') def test_bond_up_delay(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: active-backup mii-monitor-interval: 5 up-delay: 10000 dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/updelay') as f: self.assertEqual(f.read().strip(), '10000') def test_bond_arp_interval(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: balance-xor arp-ip-targets: [ 192.168.5.1 ] arp-interval: 50000 dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/arp_interval') as f: self.assertEqual(f.read().strip(), '50000') def test_bond_arp_targets(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: balance-xor arp-interval: 50000 arp-ip-targets: [ 192.168.5.1 ] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: self.assertEqual(f.read().strip(), '192.168.5.1') def test_bond_arp_all_targets(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: interfaces: [ethbn] parameters: mode: balance-xor arp-ip-targets: [192.168.5.1] arp-interval: 50000 arp-all-targets: all arp-validate: all dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/arp_all_targets') as f: self.assertEqual(f.read().strip(), 'all 1') def test_bond_mode_balance_tlb_learn_interval(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} bonds: mybond: parameters: mode: balance-tlb mii-monitor-interval: 5 learn-packet-interval: 15 interfaces: [ethbn] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertEqual(f.read().strip(), self.dev_e_client) with open('/sys/class/net/mybond/bonding/mode') as f: self.assertEqual(f.read().strip(), 'balance-tlb 5') with open('/sys/class/net/mybond/bonding/lp_interval') as f: self.assertEqual(f.read().strip(), '15') unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/bridges.py000066400000000000000000000415601457004145200205010ustar00rootroot00000000000000#!/usr/bin/python3 # # Integration tests for bridges # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2021 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def test_eth_and_bridge(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: dhcp4: yes ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr] dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.state_dhcp4(self.dev_e_client), self.dev_e2_client, self.state_dhcp4('mybr')]) self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) # ensure that they do not get managed by NM for foreign backends expected_state = (self.backend == 'NetworkManager') and 'connected' or 'unmanaged' out = subprocess.check_output(['nmcli', 'dev'], text=True) for i in [self.dev_e_client, self.dev_e2_client, 'mybr']: self.assertRegex(out, r'%s\s+(ethernet|bridge)\s+%s' % (i, expected_state)) def test_bridge_path_cost(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr] parameters: path-cost: ethbr: 50 stp: false dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) with open('/sys/class/net/mybr/brif/%s/path_cost' % self.dev_e2_client) as f: self.assertEqual(f.read().strip(), '50') def test_bridge_ageing_time(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr] parameters: ageing-time: 21 stp: false dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) with open('/sys/class/net/mybr/bridge/ageing_time') as f: self.assertEqual(f.read().strip(), '2100') def test_bridge_max_age(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr] parameters: max-age: 12 stp: false dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) with open('/sys/class/net/mybr/bridge/max_age') as f: self.assertEqual(f.read().strip(), '1200') def test_bridge_hello_time(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr] parameters: hello-time: 1 stp: false dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) with open('/sys/class/net/mybr/bridge/hello_time') as f: self.assertEqual(f.read().strip(), '100') def test_bridge_forward_delay(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr] parameters: forward-delay: 10 stp: false dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) with open('/sys/class/net/mybr/bridge/forward_delay') as f: self.assertEqual(f.read().strip(), '1000') def test_bridge_stp_false(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr] parameters: hello-time: 100000 max-age: 100000 stp: false dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) with open('/sys/class/net/mybr/bridge/stp_state') as f: self.assertEqual(f.read().strip(), '0') def test_bridge_port_priority(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr] parameters: port-priority: ethbr: 42 stp: false dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) with open('/sys/class/net/mybr/brif/%s/priority' % self.dev_e2_client) as f: self.assertEqual(f.read().strip(), '42') def test_bridge_port_hairpin(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr0: match: {name: %(ec)s} hairpin: true ethbr1: match: {name: %(e2c)s} hairpin: false bridges: mybr: interfaces: [ethbr0, ethbr1] dhcp4: false''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client]) self.assert_iface_up(self.dev_e_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 2, lines) self.assertIn(self.dev_e_client, lines[0]) self.assertIn(self.dev_e2_client, lines[1]) with open('/sys/devices/virtual/net/mybr/lower_%s/brport/hairpin_mode' % self.dev_e_client) as f: self.assertEqual(f.read().strip(), '1') with open('/sys/devices/virtual/net/mybr/lower_%s/brport/hairpin_mode' % self.dev_e2_client) as f: self.assertEqual(f.read().strip(), '0') @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' def test_bridge_mac(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: name: %(ec)s macaddress: %(ec_mac)s bridges: br0: interfaces: [ethbr] macaddress: "00:01:02:03:04:05" dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client, 'ec_mac': self.dev_e_client_mac}) self.match_veth_by_non_permanent_mac_quirk('ethbr', self.dev_e_client_mac) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('br0')]) self.assert_iface_up(self.dev_e_client, ['master br0'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('br0', ['inet 192.168.5.[0-9]+/24', 'ether 00:01:02:03:04:05']) def test_bridge_anonymous(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr]''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client, self.state_up('mybr')]) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', [], ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) def test_bridge_isolated(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s bridges: mybr: interfaces: [] addresses: [10.10.10.10/24]''' % {'r': self.backend}) self.generate_and_settle(['mybr']) self.assert_iface('mybr', ['inet 10.10.10.10/24']) def test_bridge_port_learning(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr0: match: {name: %(ec)s} port-mac-learning: true ethbr1: match: {name: %(e2c)s} port-mac-learning: false bridges: mybr: interfaces: [ethbr0, ethbr1] dhcp4: false''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client]) self.assert_iface_up(self.dev_e_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 2, lines) self.assertIn(self.dev_e_client, lines[0]) self.assertIn(self.dev_e2_client, lines[1]) with open('/sys/devices/virtual/net/mybr/lower_%s/brport/learning' % self.dev_e_client) as f: self.assertEqual(f.read().strip(), '1') with open('/sys/devices/virtual/net/mybr/lower_%s/brport/learning' % self.dev_e2_client) as f: self.assertEqual(f.read().strip(), '0') @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' @unittest.skip("NetworkManager does not support setting MAC for a bridge") def test_bridge_mac(self): pass def test_bridge_priority(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbr: match: {name: %(e2c)s} bridges: mybr: interfaces: [ethbr] parameters: priority: 16384 stp: false dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], text=True).splitlines() self.assertEqual(len(lines), 1, lines) self.assertIn(self.dev_e2_client, lines[0]) with open('/sys/class/net/mybr/bridge/priority') as f: self.assertEqual(f.read().strip(), '16384') unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/dbus.py000066400000000000000000000153701457004145200200170ustar00rootroot00000000000000#!/usr/bin/python3 # # Integration tests for netplan-dbus # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2023 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # Author: Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import json import os import signal import sys import subprocess import unittest from base import IntegrationTestsBase BUSCTL_CONFIG = [ 'busctl', '-j', 'call', '--system', 'io.netplan.Netplan', '/io/netplan/Netplan', 'io.netplan.Netplan', 'Config' ] BUSCTL_CONFIG_GET = [ 'busctl', '-j', 'call', '--system', 'io.netplan.Netplan', 'PLACEHOLDER', 'io.netplan.Netplan.Config', 'Get' ] BUSCTL_CONFIG_APPLY = [ 'busctl', '-j', 'call', '--system', 'io.netplan.Netplan', 'PLACEHOLDER', 'io.netplan.Netplan.Config', 'Apply' ] class _CommonTests(): def setUp(self): super().setUp() # If netplan-dbus is already running let's terminate it to # be sure the process is not from a binary from an old package # (before the installation of the one being tested) cmd = ['ps', '-C', 'netplan-dbus', '-o', 'pid='] out = subprocess.run(cmd, capture_output=True, text=True) if out.returncode == 0: pid = out.stdout.strip() os.kill(int(pid), signal.SIGTERM) def test_dbus_config_get(self): NETPLAN_YAML = '''network: version: 2 ethernets: %(nic)s: dhcp4: true ''' with open(self.config, 'w') as f: f.write(NETPLAN_YAML % {'nic': self.dev_e_client}) out = subprocess.run(BUSCTL_CONFIG, capture_output=True, text=True) self.assertEqual(out.returncode, 0, msg=f"Busctl Config() failed with error: {out.stderr}") out_dict = json.loads(out.stdout) config_path = out_dict.get('data')[0] self.assertNotEqual(config_path, "", msg="Got an empty response from DBUS") # The path has the following format: /io/netplan/Netplan/config/WM6X01 BUSCTL_CONFIG_GET[5] = config_path # Retrieving the config out = subprocess.run(BUSCTL_CONFIG_GET, capture_output=True, text=True) self.assertEqual(out.returncode, 0, msg=f"Busctl Get() failed with error: {out.stderr}") out_dict = json.loads(out.stdout) netplan_data = out_dict.get('data')[0] self.assertNotEqual(netplan_data, "", msg="Got an empty response from DBUS") self.assertEqual(netplan_data, NETPLAN_YAML % {'nic': self.dev_e_client}, msg="The original YAML is different from the one returned by DBUS") def test_dbus_config_set(self): BUSCTL_CONFIG_SET = [ 'busctl', '-j', 'call', '--system', 'io.netplan.Netplan', 'PLACEHOLDER', 'io.netplan.Netplan.Config', 'Set', 'ss', 'ethernets.%(nic)s.dhcp4=false' % {'nic': self.dev_e_client}, '', ] NETPLAN_YAML_BEFORE = '''network: version: 2 ethernets: %(nic)s: dhcp4: true ''' NETPLAN_YAML_AFTER = '''network: version: 2 ethernets: %(nic)s: dhcp4: false ''' with open(self.config, 'w') as f: f.write(NETPLAN_YAML_BEFORE % {'nic': self.dev_e_client}) out = subprocess.run(BUSCTL_CONFIG, capture_output=True, text=True) self.assertEqual(out.returncode, 0, msg=f"Busctl Config() failed with error: {out.stderr}") out_dict = json.loads(out.stdout) config_path = out_dict.get('data')[0] self.assertNotEqual(config_path, "", msg="Got an empty response from DBUS") # The path has the following format: /io/netplan/Netplan/config/WM6X01 BUSCTL_CONFIG_GET[5] = config_path BUSCTL_CONFIG_SET[5] = config_path # Changing the configuration out = subprocess.run(BUSCTL_CONFIG_SET, capture_output=True, text=True) self.assertEqual(out.returncode, 0, msg=f"Busctl Set() failed with error: {out.stderr}") out_dict = json.loads(out.stdout) self.assertEqual(out_dict.get('data')[0], True, msg="Set command failed") # Retrieving the configuration out = subprocess.run(BUSCTL_CONFIG_GET, capture_output=True, text=True) self.assertEqual(out.returncode, 0, msg=f"Busctl Get() failed with error: {out.stderr}") out_dict = json.loads(out.stdout) netplan_data = out_dict.get('data')[0] self.assertNotEqual(netplan_data, "", msg="Got an empty response from DBUS") self.assertEqual(NETPLAN_YAML_AFTER % {'nic': self.dev_e_client}, netplan_data, msg="The final YAML is different than expected") def test_dbus_config_apply(self): NETPLAN_YAML = '''network: version: 2 bridges: br1234: dhcp4: false ''' self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br1234'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write(NETPLAN_YAML) out = subprocess.run(BUSCTL_CONFIG, capture_output=True, text=True) self.assertEqual(out.returncode, 0, msg=f"Busctl Config() failed with error: {out.stderr}") out_dict = json.loads(out.stdout) config_path = out_dict.get('data')[0] self.assertNotEqual(config_path, "", msg="Got an empty response from DBUS") # The path has the following format: /io/netplan/Netplan/config/WM6X01 BUSCTL_CONFIG_APPLY[5] = config_path # Applying the configuration out = subprocess.run(BUSCTL_CONFIG_APPLY, capture_output=True, text=True) self.assertEqual(out.returncode, 0, msg=f"Busctl Apply() failed with error: {out.stderr}") out_dict = json.loads(out.stdout) self.assertEqual(out_dict.get('data')[0], True, msg="Apply command failed") self.assert_iface('br1234') class TestNetworkd(IntegrationTestsBase, _CommonTests): pass unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/diff.py000066400000000000000000000243441457004145200177730ustar00rootroot00000000000000#!/usr/bin/python3 # Netplan Diff integration tests. # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2023 Canonical, Ltd. # Author: Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import json import sys import subprocess import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def test_missing_netplan_ips(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dummy0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: dummy0: dhcp4: false dhcp6: false''' % {'r': self.backend}) self.generate_and_settle(['dummy0']) subprocess.call(['ip', 'addr', 'add', '1.2.3.4/24', 'dev', 'dummy0']) subprocess.call(['ip', 'addr', 'add', '1.2.3.40/24', 'dev', 'dummy0']) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) diff_ips = diff['interfaces']['dummy0']['netplan_state'].get('missing_addresses') self.assertIn('1.2.3.4/24', diff_ips) self.assertIn('1.2.3.40/24', diff_ips) def test_missing_system_ips(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dummy0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: dummy0: addresses: - 1.2.3.4/24 - 1.2.3.40/24 dhcp4: false dhcp6: false''' % {'r': self.backend}) self.generate_and_settle(['dummy0']) subprocess.call(['ip', 'addr', 'del', '1.2.3.4/24', 'dev', 'dummy0']) subprocess.call(['ip', 'addr', 'del', '1.2.3.40/24', 'dev', 'dummy0']) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) diff_ips = diff['interfaces']['dummy0']['system_state'].get('missing_addresses', []) self.assertIn('1.2.3.4/24', diff_ips) self.assertIn('1.2.3.40/24', diff_ips) def test_missing_sytem_ips_with_match(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: mynic: match: {name: %(e2c)s} addresses: - 1.2.3.4/24 - 1.2.3.40/24 dhcp6: false dhcp4: false''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e2_client]) subprocess.call(['ip', 'addr', 'del', '1.2.3.4/24', 'dev', self.dev_e2_client]) subprocess.call(['ip', 'addr', 'del', '1.2.3.40/24', 'dev', self.dev_e2_client]) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) diff_ips = diff['interfaces'][self.dev_e2_client]['system_state'].get('missing_addresses', []) netdef_id = diff['interfaces'][self.dev_e2_client]['id'] self.assertIn('1.2.3.4/24', diff_ips) self.assertIn('1.2.3.40/24', diff_ips) self.assertEqual(netdef_id, 'mynic') def test_missing_interfaces(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dummy1'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 ethernets: eth123456: dhcp4: false dhcp6: false''' % {'r': self.backend}) # Add an extra interface not present in any YAML subprocess.check_call(['ip', 'link', 'add', 'type', 'dummy', 'dev', 'dummy1']) subprocess.check_call(['ip', 'link', 'add', 'type', 'dummy', 'dev', 'dummy1']) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) missing_system = diff.get('missing_interfaces_system', {}) missing_netplan = diff.get('missing_interfaces_netplan', {}) self.assertIn('dummy1', missing_netplan) self.assertIn('eth123456', missing_system) def test_missing_system_nameservers_addresses(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dummy0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: dummy0: nameservers: addresses: - 1.1.1.1 - 8.8.8.8 addresses: - 1.2.3.4/24 dhcp4: false dhcp6: false''' % {'r': self.backend}) self.generate_and_settle(['dummy0']) subprocess.call(['resolvectl', 'dns', 'dummy0', '8.8.8.8']) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) diff_dns = diff['interfaces']['dummy0']['system_state'].get('missing_nameservers_addresses', []) self.assertIn('1.1.1.1', diff_dns) def test_missing_netplan_nameservers_addresses(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dummy0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: dummy0: nameservers: addresses: - 1.1.1.1 addresses: - 1.2.3.4/24 dhcp4: false dhcp6: false''' % {'r': self.backend}) self.generate_and_settle(['dummy0']) subprocess.call(['resolvectl', 'dns', 'dummy0', '1.1.1.1', '8.8.8.8']) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) diff_dns = diff['interfaces']['dummy0']['netplan_state'].get('missing_nameservers_addresses', []) self.assertIn('8.8.8.8', diff_dns) def test_missing_system_nameservers_search(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dummy0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: dummy0: nameservers: addresses: - 1.1.1.1 - 8.8.8.8 search: - mynet.local - mydomain.local addresses: - 1.2.3.4/24 dhcp4: false dhcp6: false''' % {'r': self.backend}) self.generate_and_settle(['dummy0']) subprocess.call(['resolvectl', 'domain', 'dummy0', 'mynet.local']) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) diff_dns = diff['interfaces']['dummy0']['system_state'].get('missing_nameservers_search', []) self.assertIn('mydomain.local', diff_dns) def test_missing_netplan_nameservers_search(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dummy0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: dummy0: nameservers: search: - mynet.local addresses: - 1.1.1.1 addresses: - 1.2.3.4/24 dhcp4: false dhcp6: false''' % {'r': self.backend}) self.generate_and_settle(['dummy0']) subprocess.call(['resolvectl', 'domain', 'dummy0', 'mynet.local', 'mydomain.local']) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) diff_dns = diff['interfaces']['dummy0']['netplan_state'].get('missing_nameservers_search', []) self.assertIn('mydomain.local', diff_dns) def test_missing_netplan_route(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dummy0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: dummy0: addresses: - 1.2.3.4/24 dhcp4: false dhcp6: false''' % {'r': self.backend}) self.generate_and_settle(['dummy0']) subprocess.call(['ip', 'route', 'add', '3.2.1.0/24', 'via', '1.2.3.4']) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) diff_routes = diff['interfaces']['dummy0']['netplan_state'].get('missing_routes', []) self.assertEqual(len(diff_routes), 1) route = diff_routes[0] self.assertEqual('3.2.1.0/24', route['to']) self.assertEqual('1.2.3.4', route['via']) self.assertEqual(2, route['family']) def test_missing_system_route(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dummy0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: dummy0: routes: - to: 3.2.1.0/24 via: 1.2.3.4 addresses: - 1.2.3.4/24 dhcp4: false dhcp6: false''' % {'r': self.backend}) self.generate_and_settle(['dummy0']) subprocess.call(['ip', 'route', 'del', '3.2.1.0/24', 'via', '1.2.3.4']) diff = json.loads(subprocess.check_output(['netplan', 'status', '--diff', '-f', 'json'])) diff_routes = diff['interfaces']['dummy0']['system_state'].get('missing_routes', []) self.assertEqual(len(diff_routes), 1) route = diff_routes[0] self.assertEqual('3.2.1.0/24', route['to']) self.assertEqual('1.2.3.4', route['via']) self.assertEqual(2, route['family']) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/dummies.py000066400000000000000000000061231457004145200205210ustar00rootroot00000000000000#!/usr/bin/python3 # Dummy devices integration tests. wokeignore:rule=dummy # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2023 Canonical, Ltd. # Author: Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def test_create_single_interface(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dm0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: # wokeignore:rule=dummy dm0: {} ''' % {'r': self.backend}) self.generate_and_settle(['dm0']) self.assert_iface('dm0') def test_create_multiple_interfaces(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dm0'], stderr=subprocess.DEVNULL) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dm1'], stderr=subprocess.DEVNULL) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dm2'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: # wokeignore:rule=dummy dm0: {} dm1: {} dm2: {} ''' % {'r': self.backend}) self.generate_and_settle(['dm0', 'dm1', 'dm2']) self.assert_iface('dm0') self.assert_iface('dm1') self.assert_iface('dm2') def test_interface_with_ip_addresses(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'dm0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 dummy-devices: # wokeignore:rule=dummy dm0: addresses: - 192.168.123.123/24 - 1234:FFFF::42/64 ''' % {'r': self.backend}) self.generate_and_settle(['dm0']) self.assert_iface('dm0', ['inet 192.168.123.123/24', 'inet6 1234:ffff::42/64']) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/ethernets.py000066400000000000000000000432331457004145200210620ustar00rootroot00000000000000#!/usr/bin/python3 # # Integration tests for ethernet devices and features common to all device # types. # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2021 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # AUthor: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsBase, nm_uses_dnsmasq, resolved_in_use, test_backends class _CommonTests(): def test_eth_mtu(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: enmtus: match: {name: %(e2c)s} mtu: 1492 dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.state_dhcp4(self.dev_e2_client)]) self.assert_iface_up(self.dev_e2_client, ['inet 192.168.6.[0-9]+/24', 'mtu 1492']) def test_eth_mac(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'set', self.dev_e2_client, 'address', self.dev_e2_client_mac]) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: enmac: match: {name: %(e2c)s} macaddress: 00:01:02:03:04:05 dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.state_dhcp4(self.dev_e2_client)]) self.assert_iface_up(self.dev_e2_client, ['inet 192.168.6.[0-9]+/24', 'ether 00:01:02:03:04:05']) def test_eth_permanent_mac(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'set', self.dev_e2_client, 'address', self.dev_e2_client_mac]) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: enmac: match: {name: %(e2c)s} macaddress: permanent dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.state_dhcp4(self.dev_e2_client)]) # The "permanent" option doesn't really work with veth interfaces but it at least # tests that the option works self.assert_iface_up(self.dev_e2_client, ['inet 192.168.6.[0-9]+/24', f'ether {self.dev_e2_client_mac}']) # Supposed to fail if tested against NetworkManager < 1.14 # Interface globbing was introduced as of NM 1.14+ def test_eth_glob(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: englob: match: {name: "eth?2"} addresses: ["172.16.42.99/18", "1234:FFFF::42/64"] ''' % {'r': self.backend}) # globbing match on "eth42", i.e. self.dev_e_client self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet 172.16.42.99/18', 'inet6 1234:ffff::42/64']) def test_manual_addresses(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: ["172.16.42.99/18", "1234:FFFF::42/64"] dhcp4: yes %(e2c)s: addresses: ["172.16.1.2/24"] gateway4: "172.16.1.1" nameservers: addresses: [172.1.2.3] search: ["fakesuffix"] ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.state_dhcp4(self.dev_e_client), self.dev_e2_client]) if self.backend == 'NetworkManager': self.nm_online_full(self.dev_e_client) self.assert_iface_up(self.dev_e_client, ['inet 172.16.42.99/18', 'inet6 1234:ffff::42/64', 'inet 192.168.5.[0-9]+/24']) # from DHCP self.assert_iface_up(self.dev_e2_client, ['inet 172.16.1.2/24']) self.assertIn(b'default via 192.168.5.1', # from DHCP subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertNotIn(b'default', subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'default via 172.16.1.1', subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e2_client])) self.assertNotIn(b'default', subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e2_client])) # ensure that they do not get managed by NM for foreign backends expected_state = (self.backend == 'NetworkManager') and 'connected' or 'unmanaged' out = subprocess.check_output(['nmcli', 'dev'], text=True) for i in [self.dev_e_client, self.dev_e2_client]: self.assertRegex(out, r'%s\s+(ethernet|bridge)\s+%s' % (i, expected_state)) with open('/etc/resolv.conf') as f: resolv_conf = f.read() if self.backend == 'NetworkManager' and nm_uses_dnsmasq: sys.stdout.write('[NM with dnsmasq] ') sys.stdout.flush() self.assertRegex(resolv_conf, 'search.*fakesuffix') # not easy to peek dnsmasq's brain, so check its logging out = subprocess.check_output(['journalctl', '--quiet', '-tdnsmasq', '-ocat', '--since=-30s'], text=True) self.assertIn('nameserver 172.1.2.3', out) elif resolved_in_use(): sys.stdout.write('[resolved] ') sys.stdout.flush() out = subprocess.check_output(['resolvectl', 'status'], text=True) self.assertIn('DNS Servers: 172.1.2.3', out) self.assertIn('fakesuffix', out) else: sys.stdout.write('[/etc/resolv.conf] ') sys.stdout.flush() self.assertRegex(resolv_conf, 'search.*fakesuffix') # /etc/resolve.conf often already has three nameserver entries if 'nameserver 172.1.2.3' not in resolv_conf: self.assertGreaterEqual(resolv_conf.count('nameserver'), 3) # change the addresses, make sure that "apply" does not leave leftovers with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: ["172.16.5.3/20", "9876:BBBB::11/70"] gateway6: "9876:BBBB::1" %(e2c)s: addresses: ["172.16.7.2/30", "4321:AAAA::99/80"] dhcp4: yes ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4(self.dev_e2_client)]) if self.backend == 'NetworkManager': self.nm_online_full(self.dev_e2_client) self.assert_iface_up(self.dev_e_client, ['inet 172.16.5.3/20'], ['inet 192.168.5', # old DHCP 'inet 172.16.42', # old static IPv4 'inet6 1234']) # old static IPv6 self.assert_iface_up(self.dev_e2_client, ['inet 172.16.7.2/30', 'inet6 4321:aaaa::99/80', 'inet 192.168.6.[0-9]+/24'], # from DHCP ['inet 172.16.1']) # old static IPv4 self.assertNotIn(b'default', subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'via 9876:bbbb::1', subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) self.assertIn(b'default via 192.168.6.1', # from DHCP subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e2_client])) self.assertNotIn(b'default', subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e2_client])) def test_dhcp6(self): self.setup_eth('slaac') with open(self.config, 'w') as f: f.write('''network: version: 2 renderer: %(r)s ethernets: %(ec)s: dhcp6: yes accept-ra: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.state_dhcp6(self.dev_e_client)]) self.assert_iface_up(self.dev_e_client, ['inet6 2600:'], ['inet 192.168']) def test_ip6_token(self): self.setup_eth('slaac') with open(self.config, 'w') as f: f.write('''network: version: 2 renderer: %(r)s ethernets: %(ec)s: dhcp6: yes accept-ra: yes ipv6-address-token: ::42''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.state_dhcp6(self.dev_e_client)]) self.assert_iface_up(self.dev_e_client, ['inet6 2600::42/64']) def test_link_local_all(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: link-local: [ ipv4, ipv6 ]''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) # Verify IPv4 and IPv6 link local addresses are there self.assert_iface(self.dev_e_client, ['inet6 fe80:', 'inet 169.254.']) def test_rename_interfaces(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: idx: match: name: %(ec)s set-name: iface1 addresses: [10.10.10.11/24] idy: match: macaddress: %(e2c_mac)s set-name: iface2 addresses: [10.10.10.22/24] ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c_mac': self.dev_e2_client_mac}) self.match_veth_by_non_permanent_mac_quirk('idy', self.dev_e2_client_mac) self.generate_and_settle(['iface1', 'iface2']) self.assert_iface_up('iface1', ['inet 10.10.10.11']) self.assert_iface_up('iface2', ['inet 10.10.10.22']) def test_link_offloading(self): self.setup_eth(None, False) # check kernel defaults out = subprocess.check_output(['ethtool', '-k', self.dev_e_client]) self.assertIn(b'rx-checksumming: on', out) self.assertIn(b'tx-checksumming: on', out) self.assertIn(b'tcp-segmentation-offload: on', out) self.assertIn(b'tx-tcp6-segmentation: on', out) self.assertIn(b'generic-segmentation-offload: on', out) # enabled for armhf on autopkgtest.u.c but 'off' elsewhere # self.assertIn(b'generic-receive-offload: off', out) # validate turning off with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: [10.10.10.22/24] receive-checksum-offload: off transmit-checksum-offload: off tcp-segmentation-offload: off tcp6-segmentation-offload: off generic-segmentation-offload: off generic-receive-offload: off #large-receive-offload: off # not possible on veth ''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet 10.10.10.22']) out = subprocess.check_output(['ethtool', '-k', self.dev_e_client]) self.assertIn(b'rx-checksumming: off', out) self.assertIn(b'tx-checksumming: off', out) self.assertIn(b'tcp-segmentation-offload: off', out) self.assertIn(b'tx-tcp6-segmentation: off', out) self.assertIn(b'generic-segmentation-offload: off', out) self.assertIn(b'generic-receive-offload: off', out) # validate turning on with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: [10.10.10.22/24] receive-checksum-offload: true transmit-checksum-offload: true tcp-segmentation-offload: true tcp6-segmentation-offload: true generic-segmentation-offload: true generic-receive-offload: true #large-receive-offload: true # not possible on veth ''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet 10.10.10.22']) out = subprocess.check_output(['ethtool', '-k', self.dev_e_client]) self.assertIn(b'rx-checksumming: on', out) self.assertIn(b'tx-checksumming: on', out) self.assertIn(b'tcp-segmentation-offload: on', out) self.assertIn(b'tx-tcp6-segmentation: on', out) self.assertIn(b'generic-segmentation-offload: on', out) self.assertIn(b'generic-receive-offload: on', out) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' def test_eth_dhcp6_off(self): self.setup_eth('slaac') with open(self.config, 'w') as f: f.write('''network: version: 2 renderer: %(r)s ethernets: %(ec)s: dhcp6: no accept-ra: yes addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.state_dhcp6(self.dev_e_client)]) self.assert_iface_up(self.dev_e_client, ['inet6 2600:'], []) def test_eth_dhcp6_off_no_accept_ra(self): self.setup_eth('slaac') with open(self.config, 'w') as f: f.write('''network: version: 2 renderer: %(r)s ethernets: %(ec)s: dhcp6: no accept-ra: no addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, [], ['inet6 2600:']) # TODO: implement link-local handling in NetworkManager backend and move this test into CommonTests() def test_link_local_ipv4(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: link-local: [ ipv4 ]''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) # Verify IPv4 link local address is there, while IPv6 is not self.assert_iface(self.dev_e_client, ['inet 169.254.'], ['inet6 fe80:']) # TODO: implement link-local handling in NetworkManager backend and move this test into CommonTests() def test_link_local_ipv6(self): self.setup_eth('ra-only') with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: link-local: [ ipv6 ]''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) # Verify IPv6 link local address is there, while IPv4 is not self.assert_iface(self.dev_e_client, ['inet6 fe80:'], ['inet 169.254.']) # TODO: implement link-local handling in NetworkManager backend and move this test into CommonTests() def test_link_local_disabled(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: ["172.16.5.3/20", "9876:BBBB::11/70"] # needed to bring up the interface at all link-local: []''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) # Verify IPv4 and IPv6 link local addresses are not there self.assert_iface(self.dev_e_client, ['inet6 9876:bbbb::11/70', 'inet 172.16.5.3/20'], ['inet6 fe80:', 'inet 169.254.']) @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' @unittest.skip("NetworkManager does not disable accept_ra: bug LP: #1704210") def test_eth_dhcp6_off(self): self.setup_eth('slaac') with open(self.config, 'w') as f: f.write('''network: version: 2 renderer: %(r)s ethernets: %(ec)s: dhcp6: no addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, [], ['inet6 2600:']) def test_eth_random_mac(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'set', self.dev_e2_client, 'address', self.dev_e2_client_mac]) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: enmac: match: {name: %(e2c)s} macaddress: random dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.state_dhcp4(self.dev_e2_client)]) out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', self.dev_e2_client], text=True) new_mac = out.split()[2] # Tests if the MAC address is different after applying the configuration self.assertNotEqual('00:11:22:33:44:55', new_mac) unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/ovs.py000066400000000000000000000721761457004145200177000ustar00rootroot00000000000000#!/usr/bin/python3 # # Integration tests for bonds # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2020-2021 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def _collect_ovs_settings(self, bridge0): d = {} d['show'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) d['ssl'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-ssl']) # Get external-ids for tbl in ('Open_vSwitch', 'Controller', 'Bridge', 'Port', 'Interface'): cols = 'name,external-ids' if tbl == 'Open_vSwitch': cols = 'external-ids' elif tbl == 'Controller': cols = '_uuid,external-ids' d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', tbl]) # Get other-config for tbl in ('Open_vSwitch', 'Bridge', 'Port', 'Interface'): cols = 'name,other-config' if tbl == 'Open_vSwitch': cols = 'other-config' d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', tbl]) # Get bond settings for col in ('bond_mode', 'lacp'): d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', 'Port']) # Get bridge settings d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-fail-mode', bridge0]) for col in ('mcast_snooping_enable', 'rstp_enable', 'protocols'): d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', 'Bridge']) # Get controller settings d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-controller', bridge0]) for col in ('connection_mode',): d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=_uuid,%s' % col, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', 'Controller']) return d def test_cleanup_interfaces(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) with open(self.config, 'w') as f: f.write('''network: openvswitch: ports: - [patch0-1, patch1-0] ethernets: # define this eth to avoid NM taking control and breaking the following .generate_and_settle() %(ec)s: {} bridges: ovs0: {interfaces: [patch0-1]} ovs1: {interfaces: [patch1-0]}''' % {'ec': self.dev_e_client}) self.generate_and_settle(['ovs0', 'ovs1']) # Basic verification that the bridges/ports/interfaces are there in OVS out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs0', out) self.assertIn(b' Port patch0-1', out) self.assertIn(b' Interface patch0-1', out) self.assertIn(b' Bridge ovs1', out) self.assertIn(b' Port patch1-0', out) self.assertIn(b' Interface patch1-0', out) with open(self.config, 'w') as f: f.write('''network: ethernets: %(ec)s: {addresses: ['1.2.3.4/24']}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) # Verify that the netplan=true tagged bridges/ports have been cleaned up out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertNotIn(b'Bridge ovs0', out) self.assertNotIn(b'Port patch0-1', out) self.assertNotIn(b'Interface patch0-1', out) self.assertNotIn(b'Bridge ovs1', out) self.assertNotIn(b'Port patch1-0', out) self.assertNotIn(b'Interface patch1-0', out) self.assert_iface_up(self.dev_e_client, ['inet 1.2.3.4/24']) def test_cleanup_patch_ports(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patchy']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) with open(self.config, 'w') as f: f.write('''network: ethernets: %(ec)s: {addresses: [10.10.10.20/24]} openvswitch: ports: [[patch0-1, patch1-0]] bonds: bond0: {interfaces: [patch1-0, %(ec)s]} bridges: ovs0: {interfaces: [patch0-1, bond0]}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'ovs0']) # Basic verification that the bridges/ports/interfaces are there in OVS out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs0', out) self.assertIn(b' Port patch0-1\n Interface patch0-1\n type: patch', out) self.assertIn(b' Port bond0', out) self.assertIn(b' Interface patch1-0\n type: patch', out) self.assertIn(b' Interface eth42', out) with open(self.config, 'w') as f: f.write('''network: ethernets: %(ec)s: {addresses: [10.10.10.20/24]} openvswitch: ports: [[patchx, patchy]] bonds: bond0: {interfaces: [patchx, %(ec)s]} bridges: ovs1: {interfaces: [patchy, bond0]}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'ovs1']) # Verify that the netplan=true tagged patch ports have been cleaned up # even though the containing bond0 port still exists (with new patch ports) out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs1', out) self.assertIn(b' Port patchy\n Interface patchy\n type: patch', out) self.assertIn(b' Port bond0', out) self.assertIn(b' Interface patchx\n type: patch', out) self.assertIn(b' Interface eth42', out) self.assertNotIn(b'Bridge ovs0', out) self.assertNotIn(b'Port patch0-1', out) self.assertNotIn(b'Interface patch0-1', out) self.assertNotIn(b'Port patch1-0', out) self.assertNotIn(b'Interface patch1-0', out) def test_bridge_vlan(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-data']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) with open(self.config, 'w') as f: f.write('''network: version: 2 ethernets: %(ec)s: mtu: 9000 bridges: br-%(ec)s: mtu: 9000 interfaces: [%(ec)s] openvswitch: {} br-data: openvswitch: {} addresses: [192.168.20.1/16] vlans: #implicitly handled by OVS because of its link br-%(ec)s.100: id: 100 link: br-%(ec)s''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'br-eth42', 'br-data', 'br-eth42.100']) # Basic verification that the interfaces/ports are set up in OVS out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertIn(b''' Port %(ec)b Interface %(ec)b''' % {b'ec': self.dev_e_client.encode()}, out) self.assertIn(b''' Port br-%(ec)b.100 tag: 100 Interface br-%(ec)b.100 type: internal''' % {b'ec': self.dev_e_client.encode()}, out) self.assertIn(b' Bridge br-data', out) self.assert_iface('br-%s' % self.dev_e_client, ['mtu 9000']) self.assert_iface('br-data', ['inet 192.168.20.1/16']) self.assert_iface(self.dev_e_client, ['mtu 9000', 'master ovs-system']) # wokeignore:rule=master self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', '-t', '5', 'br-to-vlan', 'br-%s.100' % self.dev_e_client])) self.assertIn(b'br-%b' % self.dev_e_client.encode(), subprocess.check_output( ['ovs-vsctl', '-t', '5', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) self.assertIn(b'br-%b' % self.dev_e_client.encode(), out) def test_bridge_vlan_deletion(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) with open(self.config, 'w') as f: f.write('''network: version: 2 ethernets: %(ec)s: mtu: 9000 bridges: br-%(ec)s: mtu: 9000 interfaces: [%(ec)s] openvswitch: {} vlans: #implicitly handled by OVS because of its link br-%(ec)s.100: id: 100 link: br-%(ec)s''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'br-eth42', 'br-eth42.100']) # Basic verification that the underlying bridge and vlan interface are configured out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertIn(b''' Port br-%(ec)b.100 tag: 100 Interface br-%(ec)b.100 type: internal''' % {b'ec': self.dev_e_client.encode()}, out) self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', '-t', '5', 'br-to-vlan', 'br-%s.100' % self.dev_e_client])) # Write a network configuration that has the .100 vlan interface removed with open(self.config, 'w') as f: f.write('''network: version: 2 ethernets: %(ec)s: mtu: 9000 bridges: br-%(ec)s: mtu: 9000 interfaces: [%(ec)s] openvswitch: {}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'br-eth42']) # Check that the underlying bridge is still present but the vlan interface is now absent out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertNotIn(b'Port br-%(ec)b.100' % {b'ec': self.dev_e_client.encode()}, out) def test_bridge_base(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', 'del-ssl']) with open(self.config, 'w') as f: f.write('''network: ethernets: %(ec)s: {} %(e2c)s: {} openvswitch: ssl: ca-cert: /some/ca-cert.pem certificate: /another/certificate.pem private-key: /private/key.pem bridges: ovsbr: addresses: [192.170.1.1/24] interfaces: [%(ec)s, %(e2c)s] openvswitch: fail-mode: secure controller: addresses: [tcp:127.0.0.1, "pssl:1337:[::1]", unix:/some/socket] ''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) # Basic verification that the interfaces/ports are in OVS out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovsbr', out) self.assertIn(b' Controller "tcp:127.0.0.1"', out) self.assertIn(b' Controller "pssl:1337:[::1]"', out) self.assertIn(b' Controller "unix:/some/socket"', out) self.assertIn(b' fail_mode: secure', out) self.assertIn(b' Port %(ec)b\n Interface %(ec)b' % {b'ec': self.dev_e_client.encode()}, out) self.assertIn(b' Port %(e2c)b\n Interface %(e2c)b' % {b'e2c': self.dev_e2_client.encode()}, out) # Verify the bridge was tagged 'netplan:true' correctly out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Bridge', 'ovsbr']) self.assertIn(b'netplan=true', out) self.assert_iface('ovsbr', ['inet 192.170.1.1/24']) def test_bond_base(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'mybond']) with open(self.config, 'w') as f: f.write('''network: ethernets: %(ec)s: {} %(e2c)s: {} bonds: mybond: interfaces: [%(ec)s, %(e2c)s] parameters: mode: balance-slb openvswitch: lacp: off bridges: ovsbr: addresses: [192.170.1.1/24] interfaces: [mybond]''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) # Basic verification that the interfaces/ports are in OVS out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovsbr', out) self.assertIn(b' Port mybond', out) self.assertIn(b' Interface %b' % self.dev_e_client.encode(), out) self.assertIn(b' Interface %b' % self.dev_e2_client.encode(), out) # Verify the bond was tagged 'netplan:true' correctly out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Port']) self.assertIn(b'mybond,netplan=true', out) # Verify bond params out = subprocess.check_output(['ovs-appctl', 'bond/show', 'mybond']) self.assertIn(b'---- mybond ----', out) self.assertIn(b'bond_mode: balance-slb', out) self.assertIn(b'lacp_status: off', out) self.assertRegex(out, br'(slave|member) %b: enabled' % self.dev_e_client.encode()) # wokeignore:rule=slave self.assertRegex(out, br'(slave|member) %b: enabled' % self.dev_e2_client.encode()) # wokeignore:rule=slave self.assert_iface('ovsbr', ['inet 192.170.1.1/24']) def test_bridge_patch_ports(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br0']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br1']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) with open(self.config, 'w') as f: f.write('''network: openvswitch: ports: - [patch0-1, patch1-0] bridges: br0: addresses: [192.168.1.1/24] interfaces: [patch0-1] br1: addresses: [192.168.2.1/24] interfaces: [patch1-0]''') self.generate_and_settle(['br0', 'br1']) # Basic verification that the interfaces/ports are set up in OVS out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br0', out) self.assertIn(b''' Port patch0-1 Interface patch0-1 type: patch options: {peer=patch1-0}''', out) self.assertIn(b' Bridge br1', out) self.assertIn(b''' Port patch1-0 Interface patch1-0 type: patch options: {peer=patch0-1}''', out) self.assert_iface('br0', ['inet 192.168.1.1/24']) self.assert_iface('br1', ['inet 192.168.2.1/24']) def test_bridge_non_ovs_bond(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs-br']) self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'non-ovs-bond']) with open(self.config, 'w') as f: f.write('''network: version: 2 ethernets: %(ec)s: {} %(e2c)s: {} bonds: non-ovs-bond: interfaces: [%(ec)s, %(e2c)s] bridges: ovs-br: interfaces: [non-ovs-bond] openvswitch: {}''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovs-br', 'non-ovs-bond']) # Basic verification that the interfaces/ports are set up in OVS out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], text=True) self.assertIn(' Bridge ovs-br', out) self.assertIn(''' Port non-ovs-bond Interface non-ovs-bond''', out) self.assertIn(''' Port ovs-br Interface ovs-br type: internal''', out) self.assert_iface('non-ovs-bond', ['master ovs-system']) # wokeignore:rule=master self.assert_iface(self.dev_e_client, ['master non-ovs-bond']) # wokeignore:rule=master self.assert_iface(self.dev_e2_client, ['master non-ovs-bond']) # wokeignore:rule=master def test_vlan_maas(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', '%s.21' % self.dev_e_client], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: version: 2 bridges: ovs0: addresses: [10.5.48.11/20] interfaces: [%(ec)s.21] macaddress: 00:1f:16:15:78:6f mtu: 1500 nameservers: addresses: [10.5.32.99] search: [maas] openvswitch: {} parameters: forward-delay: 15 stp: false ethernets: %(ec)s: addresses: [10.5.32.26/20] gateway4: 10.5.32.1 mtu: 1500 nameservers: addresses: [10.5.32.99] search: [maas] vlans: %(ec)s.21: id: 21 link: %(ec)s mtu: 1500''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'ovs0', 'eth42.21']) # Basic verification that the interfaces/ports are set up in OVS out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], text=True) self.assertIn(' Bridge ovs0', out) self.assertIn(''' Port %(ec)s.21 Interface %(ec)s.21''' % {'ec': self.dev_e_client}, out) self.assertIn(''' Port ovs0 Interface ovs0 type: internal''', out) self.assert_iface('ovs0', ['inet 10.5.48.11/20']) self.assert_iface_up(self.dev_e_client, ['inet 10.5.32.26/20']) self.assert_iface_up('%s.21' % self.dev_e_client, ['%(ec)s.21@%(ec)s' % {'ec': self.dev_e_client}]) def test_missing_ovs_tools(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['mv', '/usr/bin/ovs-vsctl.bak', '/usr/bin/ovs-vsctl']) subprocess.check_call(['mv', '/usr/bin/ovs-vsctl', '/usr/bin/ovs-vsctl.bak']) with open(self.config, 'w') as f: f.write('''network: version: 2 bridges: ovs0: interfaces: [%(ec)s] openvswitch: {} ethernets: %(ec)s: {}''' % {'ec': self.dev_e_client}) p = subprocess.Popen(['netplan', 'apply'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) (out, err) = p.communicate() self.assertIn('ovs0: The \'ovs-vsctl\' tool is required to setup OpenVSwitch interfaces.', err) self.assertNotEqual(p.returncode, 0) # Netplan shouldn't crash if openvswitch is installed but not running: LP#1995598 def test_ovsdb_server_is_not_running(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['systemctl', 'start', 'ovsdb-server.service']) self.addCleanup(subprocess.call, ['systemctl', 'start', 'ovs-vswitchd.service']) subprocess.check_call(['systemctl', 'stop', 'ovsdb-server.service']) with open(self.config, 'w') as f: f.write('''network: version: 2 bridges: br0: dhcp4: false''') p = subprocess.Popen(['netplan', 'apply'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) (_, err) = p.communicate() self.assertIn('Cannot call Open vSwitch: ovsdb-server.service is not running.', err) self.assertEqual(p.returncode, 0) def test_settings_tag_cleanup(self): self.setup_eth(None, False) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) with open(self.config, 'w') as f: f.write('''network: version: 2 openvswitch: protocols: [OpenFlow13, OpenFlow14, OpenFlow15] ports: - [patch0-1, patch1-0] ssl: ca-cert: /some/ca-cert.pem certificate: /another/cert.pem private-key: /private/key.pem external-ids: somekey: 55:44:33:22:11:00 other-config: key: value ethernets: %(ec)s: addresses: [10.5.32.26/20] openvswitch: external-ids: iface-id: mylocaliface other-config: disable-in-band: false %(e2c)s: {} bonds: bond0: interfaces: [patch1-0, %(e2c)s] openvswitch: lacp: passive parameters: mode: balance-tcp bridges: ovs0: addresses: [10.5.48.11/20] interfaces: [patch0-1, %(ec)s, bond0] openvswitch: protocols: [OpenFlow10, OpenFlow11, OpenFlow12] controller: addresses: [unix:/var/run/openvswitch/ovs0.mgmt] connection-mode: out-of-band fail-mode: secure mcast-snooping: true external-ids: iface-id: myhostname other-config: disable-in-band: true hwaddr: aa:bb:cc:dd:ee:ff ovs1: openvswitch: # Add ovs1 as rstp cannot be used if bridge contains a bond interface rstp: true ''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovs0', 'ovs1']) before = self._collect_ovs_settings('ovs0') subprocess.check_call(['netplan', 'apply', '--only-ovs-cleanup']) after = self._collect_ovs_settings('ovs0') # Verify interfaces for data in (before['show'], after['show']): self.assertIn(b'Bridge ovs0', data) self.assertIn(b'Port ovs0', data) self.assertIn(b'Interface ovs0', data) self.assertIn(b'Port patch0-1', data) self.assertIn(b'Interface patch0-1', data) self.assertIn(b'Port eth42', data) self.assertIn(b'Interface eth42', data) self.assertIn(b'Bridge ovs1', data) self.assertIn(b'Port ovs1', data) self.assertIn(b'Interface ovs1', data) self.assertIn(b'Port bond0', data) self.assertIn(b'Interface eth42', data) self.assertIn(b'Interface patch1-0', data) # Verify all settings tags have been removed for tbl in ('Open_vSwitch', 'Controller', 'Bridge', 'Port', 'Interface'): self.assertNotIn(b'netplan/', after['external-ids-%s' % tbl]) # Verify SSL for s in (b'Private key: /private/key.pem', b'Certificate: /another/cert.pem', b'CA Certificate: /some/ca-cert.pem'): self.assertIn(s, before['ssl']) self.assertNotIn(s, after['ssl']) # Verify Bond self.assertIn(b'bond0,balance-tcp\n', before['bond_mode-Bond']) self.assertIn(b'bond0,\n', after['bond_mode-Bond']) self.assertIn(b'bond0,passive\n', before['lacp-Bond']) self.assertIn(b'bond0,\n', after['lacp-Bond']) # Verify Bridge self.assertIn(b'secure', before['set-fail-mode-Bridge']) self.assertNotIn(b'secure', after['set-fail-mode-Bridge']) self.assertIn(b'ovs0,true\n', before['mcast_snooping_enable-Bridge']) self.assertIn(b'ovs0,false\n', after['mcast_snooping_enable-Bridge']) self.assertIn(b'ovs1,true\n', before['rstp_enable-Bridge']) self.assertIn(b'ovs1,false\n', after['rstp_enable-Bridge']) self.assertIn(b'ovs0,OpenFlow10 OpenFlow11 OpenFlow12\n', before['protocols-Bridge']) self.assertIn(b'ovs0,\n', after['protocols-Bridge']) # Verify global protocols self.assertIn(b'ovs1,OpenFlow13 OpenFlow14 OpenFlow15\n', before['protocols-Bridge']) self.assertIn(b'ovs1,\n', after['protocols-Bridge']) # Verify Controller self.assertIn(b'Controller "unix:/var/run/openvswitch/ovs0.mgmt"', before['show']) self.assertNotIn(b'Controller', after['show']) self.assertIn(b'unix:/var/run/openvswitch/ovs0.mgmt', before['set-controller-Bridge']) self.assertIn(b',out-of-band', before['connection_mode-Controller']) self.assertEqual(b'', after['set-controller-Bridge']) self.assertEqual(b'', after['connection_mode-Controller']) # Verify other-config self.assertIn(b'key=value', before['other-config-Open_vSwitch']) self.assertNotIn(b'key=value', after['other-config-Open_vSwitch']) self.assertIn(b'hwaddr=aa:bb:cc:dd:ee:ff', before['other-config-Bridge']) self.assertNotIn(b'hwaddr=aa:bb:cc:dd:ee:ff', after['other-config-Bridge']) self.assertIn(b'ovs0,disable-in-band=true', before['other-config-Bridge']) self.assertIn(b'ovs0,\n', after['other-config-Bridge']) self.assertIn(b'eth42,disable-in-band=false\n', before['other-config-Interface']) self.assertIn(b'eth42,\n', after['other-config-Interface']) # Verify external-ids self.assertIn(b'somekey=55:44:33:22:11:00', before['external-ids-Open_vSwitch']) self.assertNotIn(b'somekey=55:44:33:22:11:00', after['external-ids-Open_vSwitch']) self.assertIn(b'iface-id=myhostname', before['external-ids-Bridge']) self.assertNotIn(b'iface-id=myhostname', after['external-ids-Bridge']) self.assertIn(b'iface-id=mylocaliface', before['external-ids-Interface']) self.assertNotIn(b'iface-id=mylocaliface', after['external-ids-Interface']) for tbl in ('Bridge', 'Port'): # The netplan=true tag shall be kept unitl the interface is deleted self.assertIn(b'netplan=true', before['external-ids-%s' % tbl]) self.assertIn(b'netplan=true', after['external-ids-%s' % tbl]) @unittest.skip("For debugging only") def test_zzz_ovs_debugging(self): # Runs as the last test, to collect all logs """Display OVS logs of the previous tests""" out = subprocess.check_output(['cat', '/var/log/openvswitch/ovs-vswitchd.log'], text=True) print(out) out = subprocess.check_output(['ovsdb-tool', 'show-log'], text=True) print(out) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestOVS(IntegrationTestsBase, _CommonTests): backend = 'networkd' unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/regressions.py000066400000000000000000000145121457004145200214220ustar00rootroot00000000000000#!/usr/bin/python3 # # Regression tests to catch previously-fixed issues. # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2021 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys import signal import subprocess import time import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def test_empty_yaml_lp1795343(self): with open(self.config, 'w') as f: f.write('''''') self.generate_and_settle([]) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' def test_lp1802322_bond_mac_rename(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn1: match: {name: %(ec)s} dhcp4: no ethbn2: match: {name: %(e2c)s} dhcp4: no bonds: mybond: interfaces: [ethbn1, ethbn2] macaddress: 00:0a:f7:72:a7:28 mtu: 9000 addresses: [ 192.168.5.9/24 ] gateway4: 192.168.5.1 parameters: down-delay: 0 lacp-rate: fast mii-monitor-interval: 100 mode: 802.3ad transmit-hash-policy: layer3+4 up-delay: 0 ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'mybond']) self.assert_iface_up(self.dev_e_client, ['master mybond', '00:0a:f7:72:a7:28'], # wokeignore:rule=master ['inet ']) self.assert_iface_up(self.dev_e2_client, ['master mybond', '00:0a:f7:72:a7:28'], # wokeignore:rule=master ['inet ']) self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) with open('/sys/class/net/mybond/bonding/slaves') as f: # wokeignore:rule=slave self.assertIn(self.dev_e_client, f.read().strip()) def test_try_accept_lp1949095(self): with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2''' % {'r': self.backend}) os.chmod(self.config, mode=0o600) p = subprocess.Popen(['netplan', 'try'], bufsize=1, text=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(2) p.send_signal(signal.SIGUSR1) out, err = p.communicate(timeout=10) self.assertEqual('', err) self.assertNotIn('An error occurred:', out) self.assertIn('Configuration accepted.', out) def test_try_reject_lp1949095(self): with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2''' % {'r': self.backend}) os.chmod(self.config, mode=0o600) p = subprocess.Popen(['netplan', 'try'], bufsize=1, text=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(2) p.send_signal(signal.SIGINT) out, err = p.communicate(timeout=10) self.assertEqual('', err) self.assertNotIn('An error occurred:', out) self.assertIn('Reverting.', out) def test_apply_networkd_inactive_lp1962095(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: dhcp4: true %(e2c)s: dhcp4: true version: 2''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) # stop networkd to simulate the failure case subprocess.check_call(['systemctl', 'stop', 'systemd-networkd.service', 'systemd-networkd.socket']) self.generate_and_settle([self.state_dhcp4(self.dev_e_client), self.state_dhcp4(self.dev_e2_client)]) self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) self.assert_iface_up(self.dev_e2_client, ['inet 192.168.6.[0-9]+/24']) @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' def test_try_accept_lp1959570(self): original_env = dict(os.environ) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br54'], stderr=subprocess.DEVNULL) self.addCleanup(subprocess.call, ['mv', '/snap/bin/nmcli', '/usr/bin/nmcli'], stderr=subprocess.DEVNULL) self.addCleanup(os.environ.update, original_env) os.makedirs('/snap/bin', exist_ok=True) subprocess.call(['mv', '/usr/bin/nmcli', '/snap/bin/nmcli']) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 bridges: br54: addresses: - "10.0.0.20/24"''' % {'r': self.backend}) os.chmod(self.config, mode=0o600) del os.environ['PATH'] # clear PATH, to test for LP: #1959570 p = subprocess.Popen(['/usr/sbin/netplan', 'try'], bufsize=1, text=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(2) p.send_signal(signal.SIGUSR1) out, err = p.communicate(timeout=10) os.environ = original_env self.assertEqual('', err) self.assertNotIn('An error occurred:', out) self.assertIn('Configuration accepted.', out) unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/routing.py000066400000000000000000000415671457004145200205600ustar00rootroot00000000000000#!/usr/bin/python3 # # Integration tests for routing functions # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2021 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): # Supposed to fail if tested against NetworkManager < 1.12/1.18 # The on-link option was introduced as of NM 1.12+ (for IPv4) # The on-link option was introduced as of NM 1.18+ (for IPv6) def test_route_on_link(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} addresses: ["9876:BBBB::11/70"] routes: - to: 2001:f00f:f00f::1/64 via: 9876:BBBB::5 on-link: true''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet6 9876:bbbb::11/70']) out = subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client], text=True) # NM routes have a (default) 'metric' in between 'proto static' and 'onlink' self.assertRegex(out, r'2001:f00f:f00f::/64 via 9876:bbbb::5 proto static[^\n]* onlink') # Supposed to fail if tested against NetworkManager < 1.8 # The from option was introduced as of NM 1.8+ def test_route_from(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} addresses: ["192.168.14.2/24"] routes: - to: 10.10.10.0/24 via: 192.168.14.20 from: 192.168.14.2''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet 192.168.14.2']) out = subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client], text=True) self.assertIn('10.10.10.0/24 via 192.168.14.20 proto static src 192.168.14.2', out) # Supposed to fail if tested against NetworkManager < 1.10 # The table option was introduced as of NM 1.10+ def test_route_table(self): self.setup_eth(None) table_id = '255' # This is the 'local' FIB of /etc/iproute2/rt_tables with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} dhcp4: no addresses: [ "10.20.10.2/24" ] gateway4: 10.20.10.1 routes: - to: 10.0.0.0/8 via: 11.0.0.1 table: %(tid)s on-link: true''' % {'r': self.backend, 'ec': self.dev_e_client, 'tid': table_id}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet ']) out = subprocess.check_output(['ip', 'route', 'show', 'table', table_id, 'dev', self.dev_e_client], text=True) # NM routes have a (default) 'metric' in between 'proto static' and 'onlink' self.assertRegex(out, r'10\.0\.0\.0/8 via 11\.0\.0\.1 proto static[^\n]* onlink') @unittest.skip("fails due to networkd bug setting routes with dhcp") def test_routes_v4_with_dhcp(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: dhcp4: yes routes: - to: 10.10.10.0/24 via: 192.168.5.254 metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.state_dhcp4(self.dev_e_client)]) self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) # from DHCP self.assertIn(b'default via 192.168.5.1', # from DHCP subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'10.10.10.0/24 via 192.168.5.254', # from static route subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'metric 99', # check metric from static route subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) def test_routes_v4(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: - 192.168.5.99/24 gateway4: 192.168.5.1 routes: - to: 10.10.10.0/24 via: 192.168.5.254 metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) # from DHCP self.assertIn(b'default via 192.168.5.1', # from DHCP subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'10.10.10.0/24 via 192.168.5.254', # from DHCP subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'metric 99', # check metric from static route subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) def test_routes_v6(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: ["9876:BBBB::11/70"] gateway6: "9876:BBBB::1" routes: - to: 2001:f00f:f00f::1/64 via: 9876:BBBB::5 metric: 799''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet6 9876:bbbb::11/70']) self.assertNotIn(b'default', subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'via 9876:bbbb::1', subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) self.assertIn(b'2001:f00f:f00f::/64 via 9876:bbbb::5', subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'metric 799', subprocess.check_output(['ip', '-6', 'route', 'show', '2001:f00f:f00f::/64'])) def test_routes_default(self): self.setup_eth(None, False) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: - 192.168.5.99/24 - "9876:BBBB::11/70" routes: - to: default via: 192.168.5.1 - to: default via: "9876:BBBB::1" - to: 10.10.10.0/24 via: 192.168.5.254 metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.99/24', 'inet6 9876:bbbb::11/70']) # import pdb # pdb.set_trace() self.assertIn(b'default via 192.168.5.1', subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'via 9876:bbbb::1', subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) self.assertIn(b'10.10.10.0/24 via 192.168.5.254', subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'metric 99', # check metric from static route subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) def test_per_route_mtu(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: - 192.168.5.99/24 gateway4: 192.168.5.1 routes: - to: 10.10.10.0/24 via: 192.168.5.254 mtu: 777''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assertIn(b'mtu 777', # check mtu from static route subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) def test_per_route_congestion_window(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: - 192.168.5.99/24 gateway4: 192.168.5.1 routes: - to: 10.10.10.0/24 via: 192.168.5.254 congestion-window: 16''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assertIn(b'initcwnd 16', # check initcwnd from static route subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) def test_per_route_advertised_receive_window(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: - 192.168.5.99/24 gateway4: 192.168.5.1 routes: - to: 10.10.10.0/24 via: 192.168.5.254 advertised-receive-window: 16''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assertIn(b'initrwnd 16', # check initrwnd from static route subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) def test_link_route_v4(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: - 192.168.5.99/24 gateway4: 192.168.5.1 routes: - to: 10.10.10.0/24 scope: link metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) # from DHCP self.assertIn(b'default via 192.168.5.1', # from DHCP subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'10.10.10.0/24 proto static scope link', subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) self.assertIn(b'metric 99', # check metric from static route subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) def test_vrf_basic(self): self.setup_eth('slaac') self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'vrf0'], stderr=subprocess.DEVNULL) self.addCleanup(subprocess.call, ['ip', 'route', 'flush', 'table', '1000'], stderr=subprocess.DEVNULL) self.addCleanup(subprocess.call, ['ip', 'rule', 'del', 'from', '10.10.10.42', 'table', '1000'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: %(ec)s: addresses: [10.10.10.22/24] routes: - to: 11.11.11.0/24 via: 10.10.10.2 table: 1000 vrfs: vrf0: addresses: [10.10.10.20/24] table: 1000 interfaces: [%(ec)s] routes: - to: 10.10.0.0/16 via: 10.10.10.1 routing-policy: - from: 10.10.10.42 ''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet 10.10.10.22', 'master vrf0']) # wokeignore:rule=master self.assert_iface_up('vrf0', ['MASTER']) # wokeignore:rule=master # verify routes didn't leak into the main routing table out = subprocess.check_output(['ip', 'route', 'show'], text=True) self.assertNotIn('10.10.0.0/16', out) self.assertNotIn('11.11.11.0/24', out) # verify routes were added to the VRF's routing table out = subprocess.check_output(['ip', 'route', 'show', 'table', '1000'], text=True) self.assertIn('10.10.0.0/16 via 10.10.10.1 dev vrf0', out) self.assertIn('11.11.11.0/24 via 10.10.10.2 dev {}'.format(self.dev_e_client), out) # verify routing policy was setup correctly to the VRF's table # 'routing-policy' is not supported on NetworkManager if self.backend == 'networkd': out = subprocess.check_output(['ip', 'rule', 'show'], text=True) self.assertIn('from 10.10.10.42 lookup 1000', out) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' @unittest.skip("networkd does not handle non-unicast routes correctly yet (Invalid argument)") def test_route_type_blackhole(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} addresses: [ "10.20.10.1/24" ] routes: - to: 10.10.10.0/24 via: 10.20.10.100 type: blackhole''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet ']) self.assertIn(b'blackhole 10.10.10.0/24', subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) def test_route_type_local_lp1892272(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} addresses: [ "10.20.10.1/24" ] routes: - to: 0.0.0.0/0 type: local table: 99 - to: ::/0 type: local table: 100''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet ']) self.assertIn(b'local default', subprocess.check_output(['ip', 'route', 'show', 'table', '99', 'dev', self.dev_e_client])) self.assertIn(b'local default', subprocess.check_output(['ip', '-6', 'route', 'show', 'table', '100', 'dev', self.dev_e_client])) def test_route_scope_link_lp1805038(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} addresses: [ "10.20.0.10/16" ] routes: - to: 10.96.0.0/24''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet ']) self.assertIn(b'10.96.0.0/24 proto static scope link', subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) def test_route_with_policy(self): self.setup_eth(None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} addresses: [ "10.20.10.1/24" ] routes: - to: 40.0.0.0/24 via: 10.20.10.55 metric: 50 - to: 40.0.0.0/24 via: 10.20.10.88 table: 99 metric: 50 routing-policy: - from: 10.20.10.0/24 to: 40.0.0.0/24 table: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet ']) self.assertIn(b'to 40.0.0.0/24 lookup 99', subprocess.check_output(['ip', 'rule', 'show'])) self.assertIn(b'40.0.0.0/24 via 10.20.10.88', subprocess.check_output(['ip', 'route', 'show', 'table', '99'])) @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/run.py000077500000000000000000000050221457004145200176620ustar00rootroot00000000000000#!/usr/bin/python3 # # Test runner for netplan integration tests. # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import argparse import glob import os import subprocess import textwrap import sys tests_dir = os.path.dirname(os.path.abspath(__file__)) default_backends = ['networkd', 'NetworkManager'] fixtures = ["__init__.py", "base.py", "run.py"] possible_tests = [] testfiles = glob.glob(os.path.join(tests_dir, "*.py")) for pyfile in testfiles: filename = os.path.basename(pyfile) if filename not in fixtures: possible_tests.append(filename.split('.')[0]) def dedupe(duped_list): deduped = set() for item in duped_list: real_items = item.split(",") for real_item in real_items: deduped.add(real_item) return deduped # XXX: omg, this is ugly :) parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent(""" Test runner for netplan integration tests Available tests: {} """.format("\n".join(" - {}".format(x) for x in sorted(possible_tests))))) parser.add_argument('--test', action='append', help="List of tests to be run") parser.add_argument('--backend', action='append', help="List of backends to test (NetworkManager, networkd)") args = parser.parse_args() requested_tests = set() backends = set() if args.test is not None: requested_tests = dedupe(args.test) else: requested_tests.update(possible_tests) if args.backend is not None: backends = dedupe(args.backend) else: backends.update(default_backends) os.environ["NETPLAN_TEST_BACKENDS"] = ",".join(backends) returncode = 0 for test in requested_tests: ret = subprocess.call(['python3', os.path.join(tests_dir, "{}.py".format(test))]) if returncode == 0 and ret != 0: returncode = ret sys.exit(returncode) netplan-1.0/tests/integration/scenarios.py000066400000000000000000000125671457004145200210550ustar00rootroot00000000000000#!/usr/bin/python3 # # Integration tests for complex networking scenarios # (ie. mixes of various features, may test real live cases) # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2021 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import shutil import sys import subprocess import tempfile import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def test_mix_bridge_on_bond(self): self.setup_eth('ra-only') self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s bridges: br0: interfaces: [bond0] addresses: ['192.168.0.2/24'] bonds: bond0: interfaces: [ethb2] parameters: mode: balance-rr ethernets: ethbn: match: {name: %(ec)s} ethb2: match: {name: %(e2c)s} ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'br0', 'bond0']) self.assert_iface_up(self.dev_e2_client, ['master bond0'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('bond0', ['master br0']) # wokeignore:rule=master self.assert_iface('br0', ['inet 192.168.0.2/24']) with open('/sys/class/net/bond0/bonding/slaves') as f: # wokeignore:rule=slave result = f.read().strip() self.assertIn(self.dev_e2_client, result) def test_mix_vlan_on_bridge_on_bond(self): self.setup_eth('ra-only') self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br1'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 vlans: vlan1: link: 'br0' id: 1 addresses: [ '10.10.10.1/24' ] bridges: br0: interfaces: ['bond0', 'vlan2'] parameters: stp: false path-cost: bond0: 1000 vlan2: 2000 bonds: bond0: interfaces: ['br1'] parameters: mode: balance-rr bridges: br1: interfaces: ['ethb2'] vlans: vlan2: link: ethbn id: 2 ethernets: ethbn: match: {name: %(ec)s} ethb2: match: {name: %(e2c)s} ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'br0', 'br1', 'bond0', 'vlan1', 'vlan2']) self.assert_iface_up('vlan1', ['vlan1@br0']) self.assert_iface_up('vlan2', ['vlan2@' + self.dev_e_client, 'master br0']) # wokeignore:rule=master self.assert_iface_up(self.dev_e2_client, ['master br1'], ['inet ']) # wokeignore:rule=master self.assert_iface_up('bond0', ['master br0']) # wokeignore:rule=master # https://bugs.launchpad.net/netplan/+bug/1943120 def test_remove_virtual_interfaces(self): tempdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tempdir) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br54'], stderr=subprocess.DEVNULL) confdir = os.path.join(tempdir, 'etc', 'netplan') os.makedirs(confdir) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 bridges: br54: addresses: [1.2.3.4/24]''' % {'r': self.backend}) self.generate_and_settle(['br54']) self.assert_iface('br54', ['inet 1.2.3.4/24']) # backup the current YAML state (incl. br54) shutil.copytree('/etc/netplan', confdir, dirs_exist_ok=True) # drop br54 interface subprocess.check_call(['netplan', 'set', 'network.bridges.br54.addresses=null']) self.generate_and_settle([], state_dir=tempdir) res = subprocess.run(['ip', 'link', 'show', 'dev', 'br54'], capture_output=True, text=True) self.assertIn('not exist', res.stderr) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/tunnels.py000066400000000000000000000327601457004145200205540ustar00rootroot00000000000000#!/usr/bin/python3 # Tunnel integration tests. NM and networkd are started on the generated # configuration, using emulated ethernets (veth). # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2021 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def test_tunnel_sit(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'sit-tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: sit-tun0: mode: sit local: 192.168.5.1 remote: 99.99.99.99 ''' % {'r': self.backend}) self.generate_and_settle(['sit-tun0']) self.assert_iface('sit-tun0', ['sit-tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) def test_tunnel_sit_without_local_address(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'sit-tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: sit-tun0: mode: sit remote: 99.99.99.99 ''' % {'r': self.backend}) self.generate_and_settle(['sit-tun0']) self.assert_iface('sit-tun0', ['sit-tun0@NONE', 'link.* 0.0.0.0 peer 99.99.99.99']) def test_tunnel_ipip(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: ipip local: 192.168.5.1 remote: 99.99.99.99 ttl: 64 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) def test_tunnel_ipip_without_local_address(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: ipip remote: 99.99.99.99 ttl: 64 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) self.assert_iface('tun0', ['tun0@NONE', 'link.* 0.0.0.0 peer 99.99.99.99']) def test_tunnel_wireguard(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'wg0'], stderr=subprocess.DEVNULL) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'wg1'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: wg0: #server mode: wireguard addresses: [10.10.10.20/24] gateway4: 10.10.10.21 key: 4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8= mark: 42 port: 51820 peers: - keys: public: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= shared: 7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= allowed-ips: [20.20.20.10/24] wg1: #client mode: wireguard addresses: [20.20.20.10/24] gateway4: 20.20.20.11 key: KPt9BzQjejRerEv8RMaFlpsD675gNexELOQRXt/AcH0= peers: - endpoint: 10.10.10.20:51820 allowed-ips: [0.0.0.0/0] keys: public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= shared: 7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= keepalive: 21 ''' % {'r': self.backend}) self.generate_and_settle(['wg0', 'wg1']) # Wait for handshake/connection between client & server self.wait_output(['wg', 'show', 'wg0'], 'latest handshake') self.wait_output(['wg', 'show', 'wg1'], 'latest handshake') # Verify server out = subprocess.check_output(['wg', 'show', 'wg0', 'private-key'], text=True) self.assertIn("4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=", out) out = subprocess.check_output(['wg', 'show', 'wg0', 'preshared-keys'], text=True) self.assertIn("7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8=", out) out = subprocess.check_output(['wg', 'show', 'wg0'], text=True) self.assertIn("public key: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc=", out) self.assertIn("listening port: 51820", out) self.assertIn("fwmark: 0x2a", out) self.assertIn("peer: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=", out) self.assertIn("allowed ips: 20.20.20.0/24", out) self.assertRegex(out, r'latest handshake: (\d+ seconds? ago|Now)') self.assertRegex(out, r'transfer: \d+.*B received, \d+.*B sent') self.assert_iface('wg0', ['inet 10.10.10.20/24']) # Verify client out = subprocess.check_output(['wg', 'show', 'wg1', 'private-key'], text=True) self.assertIn("KPt9BzQjejRerEv8RMaFlpsD675gNexELOQRXt/AcH0=", out) out = subprocess.check_output(['wg', 'show', 'wg1', 'preshared-keys'], text=True) self.assertIn("7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8=", out) out = subprocess.check_output(['wg', 'show', 'wg1'], text=True) self.assertIn("public key: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=", out) self.assertIn("peer: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc=", out) self.assertIn("endpoint: 10.10.10.20:51820", out) self.assertIn("allowed ips: 0.0.0.0/0", out) self.assertIn("persistent keepalive: every 21 seconds", out) self.assertRegex(out, r'latest handshake: (\d+ seconds? ago|Now)') self.assertRegex(out, r'transfer: \d+.*B received, \d+.*B sent') self.assert_iface('wg1', ['inet 20.20.20.10/24']) def test_tunnel_gre(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: gre local: 192.168.5.1 remote: 99.99.99.99 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) def test_tunnel_gre_without_local_address(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: gre remote: 99.99.99.99 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) self.assert_iface('tun0', ['tun0@NONE', 'link.* 0.0.0.0 peer 99.99.99.99']) def test_tunnel_gre6(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: ip6gre local: fe80::1 remote: 2001:dead:beef::2 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) self.assert_iface('tun0', ['tun0@NONE', 'link.* fe80::1 brd 2001:dead:beef::2']) def test_tunnel_gre_with_keys(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: gre keys: input: 1234 output: 5678 local: 192.168.5.1 remote: 99.99.99.99 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) out = subprocess.check_output(['ip', 'tunnel', 'show', 'tun0'], text=True) self.assertIn("ikey 1234 okey 5678", out) def test_tunnel_gre6_with_keys(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: ip6gre key: 1234 local: fe80::1 remote: 2001:dead:beef::2 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) self.assert_iface('tun0', ['tun0@NONE', 'link.* fe80::1 brd 2001:dead:beef::2']) out = subprocess.check_output(['ip', '-6', 'tunnel', 'show', 'tun0'], text=True) self.assertIn("key 1234", out) def test_tunnel_vxlan(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'vx0'], stderr=subprocess.DEVNULL) self.setup_eth(None, False) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: vx0: mode: vxlan id: 1337 link: %(ec)s local: 10.10.10.42 remote: 224.0.0.5 # multicast group ttl: 64 aging: 100 port: 4567 port-range: [4000, 4200] mac-learning: false short-circuit: true notifications: [l2-miss, l3-miss] checksums: [udp, zero-udp6-tx, zero-udp6-rx, remote-tx, remote-rx] # sd-networkd only ethernets: %(ec)s: addresses: [10.10.10.42/24] ''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'vx0']) self.assert_iface('vx0', ['vxlan ', ' id 1337 ', ' group 224.0.0.5 ', ' local 10.10.10.42 ', ' srcport 4000 4200 ', ' dev %s ' % self.dev_e_client, ' dstport 4567 ', ' rsc ', ' l2miss ', ' l3miss ', ' ttl 64 ', ' ageing 100 ']) if self.backend == 'networkd': # checksums are not supported on the NetworkManager backend json = self.iface_json('vx0') data = json.get('linkinfo', {}).get('info_data', {}) self.assertTrue(data.get('udp_csum')) self.assertTrue(data.get('udp_zero_csum6_tx')) self.assertTrue(data.get('udp_zero_csum6_rx')) self.assertTrue(data.get('remcsum_tx')) self.assertTrue(data.get('remcsum_rx')) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' def test_tunnel_vti(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: vti keys: 1234 local: 192.168.5.1 remote: 99.99.99.99 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) def test_tunnel_vti6(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: vti6 keys: 1234 local: fe80::1 remote: 2001:dead:beef::2 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) self.assert_iface('tun0', ['tun0@NONE', 'link.* fe80::1 brd 2001:dead:beef::2']) def test_tunnel_gretap_with_keys(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: gretap keys: input: 1.2.3.4 output: 5.6.7.8 local: 192.168.5.1 remote: 99.99.99.99 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) out = subprocess.check_output(['ip', '-details', 'link', 'show', 'tun0'], text=True) self.assertIn("gretap remote 99.99.99.99 local 192.168.5.1", out) self.assertIn("ikey 1.2.3.4 okey 5.6.7.8", out) def test_tunnel_gretap6_with_keys(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 tunnels: tun0: mode: ip6gretap keys: 1.2.3.4 local: fe80::1 remote: 2001:dead:beef::2 ''' % {'r': self.backend}) self.generate_and_settle(['tun0']) out = subprocess.check_output(['ip', '-details', 'link', 'show', 'tun0'], text=True) self.assertIn("gretap remote 2001:dead:beef::2 local fe80::1", out) self.assertIn("ikey 1.2.3.4 okey 1.2.3.4", out) @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/veths.py000066400000000000000000000054751457004145200202200ustar00rootroot00000000000000#!/usr/bin/python3 # Veths integration tests. # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2023 Canonical, Ltd. # Author: Danilo Egea Gondolfo # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def test_create_veth_pair(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'veth0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 virtual-ethernets: veth0: dhcp4: false dhcp6: false peer: veth1 veth1: dhcp4: false dhcp6: false peer: veth0''' % {'r': self.backend}) self.generate_and_settle(['veth0', 'veth1']) self.assert_iface_up('veth0') self.assert_iface_up('veth1') def test_create_veth_pair_with_ip_address(self): self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'veth0'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s version: 2 virtual-ethernets: veth0: dhcp4: false dhcp6: false peer: veth1 addresses: - 192.168.123.123/24 - 1234:FFFF::42/64 veth1: dhcp4: false dhcp6: false peer: veth0''' % {'r': self.backend}) self.generate_and_settle(['veth0', 'veth1']) self.assert_iface_up('veth0') self.assert_iface_up('veth1') expected_ips = {'192.168.123.123', '1234:ffff::42'} json = self.iface_json('veth0') data = json.get('addr_info', {}) ips = {ip.get('local') for ip in data} self.assertTrue(expected_ips.issubset(ips)) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/vlans.py000066400000000000000000000074501457004145200202050ustar00rootroot00000000000000#!/usr/bin/python3 # # Integration tests for VLAN virtual devices # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2021 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsBase, test_backends class _CommonTests(): def test_vlan(self): # we create two VLANs on e2c, and run dnsmasq on ID 2002 to test DHCP via VLAN self.setup_eth(None, start_dnsmasq=False) self.start_dnsmasq(None, self.dev_e2_ap) subprocess.check_call(['ip', 'link', 'add', 'link', self.dev_e2_ap, 'name', 'nptestsrv', 'type', 'vlan', 'id', '2002']) subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', 'nptestsrv']) subprocess.check_call(['ip', 'link', 'set', 'nptestsrv', 'up']) self.start_dnsmasq(None, 'nptestsrv') with open(self.config, 'w') as f: f.write('''network: version: 2 renderer: %(r)s ethernets: myether: match: {name: %(e2c)s} dhcp4: yes vlans: nptestone: id: 1001 link: myether addresses: [10.9.8.7/24] nptesttwo: id: 2002 link: myether dhcp4: true ''' % {'r': self.backend, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.state_dhcp4(self.dev_e2_client), 'nptestone', self.state_dhcp4('nptesttwo')]) self.assert_iface_up('nptestone', ['nptestone@' + self.dev_e2_client, 'inet 10.9.8.7/24']) self.assert_iface_up('nptesttwo', ['nptesttwo@' + self.dev_e2_client, 'inet 192.168.5']) self.assertNotIn(b'default', subprocess.check_output(['ip', 'route', 'show', 'dev', 'nptestone'])) self.assertIn(b'default via 192.168.5.1', # from DHCP subprocess.check_output(['ip', 'route', 'show', 'dev', 'nptesttwo'])) def test_vlan_mac_address(self): self.setup_eth('ra-only') self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'myvlan'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s ethernets: ethbn: match: {name: %(ec)s} vlans: myvlan: id: 101 link: ethbn macaddress: aa:bb:cc:dd:ee:22 ''' % {'r': self.backend, 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'myvlan']) self.assert_iface_up('myvlan', ['myvlan@' + self.dev_e_client]) with open('/sys/class/net/myvlan/address') as f: self.assertEqual(f.read().strip(), 'aa:bb:cc:dd:ee:22') @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsBase, _CommonTests): backend = 'networkd' @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsBase, _CommonTests): backend = 'NetworkManager' unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/integration/wifi.py000066400000000000000000000156451457004145200200250ustar00rootroot00000000000000#!/usr/bin/python3 # # Integration tests for wireless devices # # These need to be run in a VM and do change the system # configuration. # # Copyright (C) 2018-2021 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import subprocess import unittest from base import IntegrationTestsWifi, test_backends class _CommonTests(): @unittest.skip("Unsupported matching by driver / wifi matching makes this untestable for now") def test_mapping_for_driver(self): self.setup_ap('hw_mode=b\nchannel=1\nssid=fake net', None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s wifis: wifi_ifs: match: driver: mac80211_hwsim dhcp4: yes access-points: "fake net": {} decoy: {}''' % {'r': self.backend}) self.generate_and_settle([self.state_dhcp4(self.dev_w_client)]) p = subprocess.Popen(['netplan', 'generate', '--mapping', 'mac80211_hwsim'], stdout=subprocess.PIPE) out = p.communicate()[0] self.assertEquals(p.returncode, 1) self.assertIn(b'mac80211_hwsim', out) def test_wifi_ipv4_open(self): self.setup_ap('hw_mode=b\nchannel=1\nssid=fake net', None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s wifis: %(wc)s: dhcp4: yes access-points: "fake net": {} decoy: {}''' % {'r': self.backend, 'wc': self.dev_w_client}) self.generate_and_settle([self.state_dhcp4(self.dev_w_client)]) self.assert_iface_up(self.dev_w_client, ['inet 192.168.5.[0-9]+/24']) self.assertIn(b'default via 192.168.5.1', # from DHCP subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_w_client])) if self.backend == 'NetworkManager': out = subprocess.check_output(['nmcli', 'dev', 'show', self.dev_w_client], text=True) self.assertRegex(out, 'GENERAL.CONNECTION.*netplan-%s-fake net' % self.dev_w_client) self.assertRegex(out, 'IP4.DNS.*192.168.5.1') else: out = subprocess.check_output(['networkctl', 'status', self.dev_w_client], text=True) self.assertRegex(out, 'DNS.*192.168.5.1') def test_wifi_ipv4_wpa2(self): self.setup_ap('''hw_mode=g channel=1 ssid=fake net wpa=1 wpa_key_mgmt=WPA-PSK wpa_pairwise=TKIP wpa_passphrase=12345678 ''', None) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s wifis: %(wc)s: dhcp4: yes access-points: "fake net": password: 12345678 decoy: {}''' % {'r': self.backend, 'wc': self.dev_w_client}) self.generate_and_settle([self.state_dhcp4(self.dev_w_client)]) self.assert_iface_up(self.dev_w_client, ['inet 192.168.5.[0-9]+/24']) self.assertIn(b'default via 192.168.5.1', # from DHCP subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_w_client])) if self.backend == 'NetworkManager': out = subprocess.check_output(['nmcli', 'dev', 'show', self.dev_w_client], text=True) self.assertRegex(out, 'GENERAL.CONNECTION.*netplan-%s-fake net' % self.dev_w_client) self.assertRegex(out, 'IP4.DNS.*192.168.5.1') else: out = subprocess.check_output(['networkctl', 'status', self.dev_w_client], text=True) self.assertRegex(out, 'DNS.*192.168.5.1') def test_wifi_regdom(self): self.setup_ap('''hw_mode=g channel=1 ssid=fake net wpa=1 wpa_key_mgmt=WPA-PSK wpa_pairwise=TKIP wpa_passphrase=12345678 ''', None) out = subprocess.check_output(['iw', 'reg', 'get'], text=True) self.assertNotIn('country GB', out) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s wifis: %(wc)s: addresses: ["192.168.1.42/24"] regulatory-domain: GB access-points: "fake net": password: 12345678''' % {'r': self.backend, 'wc': self.dev_w_client}) self.generate_and_settle([self.dev_w_client]) self.assert_iface_up(self.dev_w_client, ['inet 192.168.1.42/24']) out = subprocess.check_output(['iw', 'reg', 'get'], text=True) self.assertIn('global\ncountry GB', out) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") class TestNetworkd(IntegrationTestsWifi, _CommonTests): backend = 'networkd' @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") class TestNetworkManager(IntegrationTestsWifi, _CommonTests): backend = 'NetworkManager' def test_wifi_ap_open(self): # we use dev_w_client and dev_w_ap in switched roles here, to keep the # existing device denylisting in NM; i. e. dev_w_client is the # NM-managed AP, and dev_w_ap the manually managed client with open(self.config, 'w') as f: f.write('''network: wifis: renderer: NetworkManager %(wc)s: dhcp4: yes access-points: "fake net": mode: ap''' % {'wc': self.dev_w_client}) self.generate_and_settle([self.state(self.dev_w_client, 'inet 10.')]) out = subprocess.check_output(['iw', 'dev', self.dev_w_client, 'info'], text=True) self.assertIn('type AP', out) self.assertIn('ssid fake net', out) # connect the other end subprocess.check_call(['ip', 'link', 'set', self.dev_w_ap, 'up']) subprocess.check_call(['iw', 'dev', self.dev_w_ap, 'connect', 'fake net']) out = subprocess.check_output(['dhclient', '-1', '-v', self.dev_w_ap], stderr=subprocess.STDOUT, text=True) self.assertIn('DHCPACK', out) out = subprocess.check_output(['iw', 'dev', self.dev_w_ap, 'info'], text=True) self.assertIn('type managed', out) self.assertIn('ssid fake net', out) self.assert_iface_up(self.dev_w_ap, ['inet 10.']) unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) netplan-1.0/tests/netplan_dbus/000077500000000000000000000000001457004145200166355ustar00rootroot00000000000000netplan-1.0/tests/netplan_dbus/__init__.py000066400000000000000000000000001457004145200207340ustar00rootroot00000000000000netplan-1.0/tests/netplan_dbus/test_dbus.py000066400000000000000000001035371457004145200212140ustar00rootroot00000000000000# # Copyright (C) 2019-2020 Canonical, Ltd. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import shutil import subprocess import tempfile import unittest import time from tests.test_utils import MockCmd rootdir = os.path.dirname(os.path.dirname( os.path.dirname(os.path.abspath(__file__)))) exe_cli = [os.path.join(rootdir, 'src', 'netplan.script')] if shutil.which('python3-coverage'): exe_cli = ['python3-coverage', 'run', '--append', '--'] + exe_cli NETPLAN_DBUS_CMD = os.environ.get('NETPLAN_DBUS_CMD', os.path.join(os.path.dirname(__file__), "..", "..", "netplan-dbus")) class TestNetplanDBus(unittest.TestCase): def setUp(self): self.tmp = tempfile.mkdtemp() os.makedirs(os.path.join(self.tmp, "etc", "netplan"), 0o700) os.makedirs(os.path.join(self.tmp, "lib", "netplan"), 0o700) os.makedirs(os.path.join(self.tmp, "run", "netplan"), 0o700) self._netplan_try_stamp = os.path.join(self.tmp, 'run', 'netplan', 'netplan-try.ready') # Create main test YAML in /etc/netplan/ test_file = os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml') with open(test_file, 'w') as f: f.write("""network: version: 2 ethernets: eth0: dhcp4: true""") self.addCleanup(shutil.rmtree, self.tmp) self.mock_netplan_cmd = MockCmd("netplan") self._create_mock_system_bus() self._run_netplan_dbus_on_mock_bus() self._mock_snap_env() self.mock_busctl_cmd = MockCmd("busctl") def _mock_snap_env(self): os.environ["SNAP"] = "test-netplan-apply-snapd" def _create_mock_system_bus(self): env = {} output = subprocess.check_output(["dbus-launch"], env={}) for s in output.decode("utf-8").split("\n"): if s == "": continue k, v = s.split("=", 1) env[k] = v # override system bus with the fake one os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = env["DBUS_SESSION_BUS_ADDRESS"] self.addCleanup(os.kill, int(env["DBUS_SESSION_BUS_PID"]), 15) def _run_netplan_dbus_on_mock_bus(self): # run netplan-dbus in a fake system bus os.environ["DBUS_TEST_NETPLAN_CMD"] = self.mock_netplan_cmd.path os.environ["DBUS_TEST_NETPLAN_ROOT"] = self.tmp p = subprocess.Popen(NETPLAN_DBUS_CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(1) # Give some time for our dbus daemon to be ready self.addCleanup(self._cleanup_netplan_dbus, p) def _cleanup_netplan_dbus(self, p): p.terminate() p.wait() # netplan-dbus does not produce output self.assertEqual(p.stdout.read(), b"") self.assertEqual(p.stderr.read(), b"") def _check_dbus_error(self, cmd, returncode=1): p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.wait() self.assertEqual(p.returncode, returncode) self.assertEqual(p.stdout.read().decode("utf-8"), "") return p.stderr.read().decode("utf-8") def _new_config_object(self): BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "Config", ] # Create new config object / config state out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertIn(b'o "/io/netplan/Netplan/config/', out) cid = out.decode('utf-8').split('/')[-1].replace('"\n', '') # Verify that the state folders were created in /tmp tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid) self.assertTrue(os.path.isdir(tmpdir)) self.assertTrue(os.path.isdir(os.path.join(tmpdir, 'etc', 'netplan'))) self.assertTrue(os.path.isdir(os.path.join(tmpdir, 'run', 'netplan'))) self.assertTrue(os.path.isdir(os.path.join(tmpdir, 'lib', 'netplan'))) # Return random config ID return cid def test_netplan_apply_in_snap_uses_dbus(self): p = subprocess.Popen( exe_cli + ["apply"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(p.stdout.read(), b"") self.assertEqual(p.stderr.read(), b"") self.assertEqual(self.mock_netplan_cmd.calls(), [ ["netplan", "apply"], ]) def test_netplan_apply_in_snap_calls_busctl(self): newenv = os.environ.copy() busctlDir = os.path.dirname(self.mock_busctl_cmd.path) newenv["PATH"] = busctlDir+":"+os.environ["PATH"] p = subprocess.Popen( exe_cli + ["apply"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=newenv) p.wait(10) self.assertEqual(p.stdout.read(), b"") self.assertEqual(p.stderr.read(), b"") self.assertEqual(self.mock_busctl_cmd.calls(), [ ["busctl", "call", "--quiet", "--system", "io.netplan.Netplan", # the service "/io/netplan/Netplan", # the object "io.netplan.Netplan", # the interface "Apply", # the method ], ]) def test_netplan_apply_in_snap_calls_busctl_ret130(self): newenv = os.environ.copy() busctlDir = os.path.dirname(self.mock_busctl_cmd.path) newenv["PATH"] = busctlDir+":"+os.environ["PATH"] self.mock_busctl_cmd.set_returncode(130) p = subprocess.Popen( exe_cli + ["apply"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=newenv) p.wait(10) # exit_on_error is True by default, so we check the returncode directly self.assertEqual(p.returncode, 130) def test_netplan_apply_in_snap_calls_busctl_err(self): newenv = os.environ.copy() busctlDir = os.path.dirname(self.mock_busctl_cmd.path) newenv["PATH"] = busctlDir+":"+os.environ["PATH"] self.mock_busctl_cmd.set_returncode(1) p = subprocess.Popen( exe_cli + ["apply"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=newenv) p.wait(10) # exit_on_error is True by default, so we check the returncode directly self.assertEqual(p.returncode, 1) def test_netplan_generate_in_snap_calls_busctl(self): newenv = os.environ.copy() busctlDir = os.path.dirname(self.mock_busctl_cmd.path) newenv["PATH"] = busctlDir+":"+os.environ["PATH"] p = subprocess.Popen( exe_cli + ["generate"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=newenv) p.wait(10) self.assertEqual(p.stdout.read(), b"") self.assertEqual(p.stderr.read(), b"") self.assertEqual(self.mock_busctl_cmd.calls(), [ ["busctl", "call", "--quiet", "--system", "io.netplan.Netplan", # the service "/io/netplan/Netplan", # the object "io.netplan.Netplan", # the interface "Generate", # the method ], ]) def test_netplan_generate_in_snap_calls_busctl_ret130(self): newenv = os.environ.copy() busctlDir = os.path.dirname(self.mock_busctl_cmd.path) newenv["PATH"] = busctlDir+":"+os.environ["PATH"] self.mock_busctl_cmd.set_returncode(130) p = subprocess.Popen( exe_cli + ["generate"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=newenv) p.wait(10) self.assertIn(b"PermissionError: failed to communicate with dbus service", p.stderr.read()) def test_netplan_generate_in_snap_calls_busctl_err(self): newenv = os.environ.copy() busctlDir = os.path.dirname(self.mock_busctl_cmd.path) newenv["PATH"] = busctlDir+":"+os.environ["PATH"] self.mock_busctl_cmd.set_returncode(1) p = subprocess.Popen( exe_cli + ["generate"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=newenv) p.wait(10) self.assertIn(b"RuntimeError: failed to communicate with dbus service: error 1", p.stderr.read()) def test_netplan_dbus_noroot(self): # Process should fail instantly, if not: kill it after 5 sec r = subprocess.run(NETPLAN_DBUS_CMD, timeout=5, capture_output=True) self.assertEqual(r.returncode, 1) self.assertIn(b'Failed to acquire service name', r.stderr) def test_netplan_dbus_happy(self): BUSCTL_NETPLAN_APPLY = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "Apply", ] output = subprocess.check_output(BUSCTL_NETPLAN_APPLY) self.assertEqual(output.decode("utf-8"), "b true\n") # one call to netplan apply in total self.assertEqual(self.mock_netplan_cmd.calls(), [ ["netplan", "apply"], ]) # and again! output = subprocess.check_output(BUSCTL_NETPLAN_APPLY) self.assertEqual(output.decode("utf-8"), "b true\n") # and another call to netplan apply self.assertEqual(self.mock_netplan_cmd.calls(), [ ["netplan", "apply"], ["netplan", "apply"], ]) def test_netplan_dbus_generate(self): BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "Generate", ] output = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(output.decode("utf-8"), "b true\n") # one call to netplan apply in total self.assertEqual(self.mock_netplan_cmd.calls(), [ ["netplan", "generate"], ]) def test_netplan_dbus_info(self): BUSCTL_NETPLAN_INFO = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "Info", ] output = subprocess.check_output(BUSCTL_NETPLAN_INFO) self.assertIn("Features", output.decode("utf-8")) def test_netplan_dbus_config(self): # Create test YAML test_file_lib = os.path.join(self.tmp, 'lib', 'netplan', 'lib_test.yaml') with open(test_file_lib, 'w') as f: f.write('TESTING-lib') test_file_run = os.path.join(self.tmp, 'run', 'netplan', 'run_test.yaml') with open(test_file_run, 'w') as f: f.write('TESTING-run') self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'lib_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'run_test.yaml'))) cid = self._new_config_object() tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid) self.addClassCleanup(shutil.rmtree, tmpdir) # Verify the object path has been created, by calling .Config.Get() on that object # it would throw an error if it does not exist BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Get", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD, text=True) self.assertIn(r's ""', out) # No output as 'netplan get' is actually mocked self.assertEqual(self.mock_netplan_cmd.calls(), [[ "netplan", "get", "all", "--root-dir={}".format(tmpdir) ]]) # Verify all *.yaml files have been copied self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'etc', 'netplan', 'main_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'lib', 'netplan', 'lib_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'run', 'netplan', 'run_test.yaml'))) def test_netplan_dbus_no_such_command(self): err = self._check_dbus_error([ "busctl", "call", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "NoSuchCommand" ]) self.assertIn("Unknown method", err) def test_netplan_dbus_config_set(self): cid = self._new_config_object() tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid) self.addCleanup(shutil.rmtree, tmpdir) # Verify .Config.Set() on the config object # No actual YAML file will be created, as the netplan command is mocked BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth42.dhcp6=true", "", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) self.assertEqual(self.mock_netplan_cmd.calls(), [[ "netplan", "set", "ethernets.eth42.dhcp6=true", "--root-dir={}".format(tmpdir) ]]) def test_netplan_dbus_config_get(self): cid = self._new_config_object() tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid) self.addCleanup(shutil.rmtree, tmpdir) # Verify .Config.Get() on the config object self.mock_netplan_cmd.set_output("network:\n eth42:\n dhcp6: true") BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Get", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD, text=True) self.assertIn(r's "network:\n eth42:\n dhcp6: true\n"', out) self.assertEqual(self.mock_netplan_cmd.calls(), [[ "netplan", "get", "all", "--root-dir={}".format(tmpdir) ]]) def test_netplan_dbus_config_cancel(self): cid = self._new_config_object() tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid) backup = self.tmp + '/run/netplan/config-BACKUP' # Verify .Config.Cancel() teardown of the config object and state dirs BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Cancel", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) time.sleep(1) # Give some time for 'Cancel' to clean up self.assertFalse(os.path.isdir(tmpdir)) # Verify the object is gone from the bus err = self._check_dbus_error(BUSCTL_NETPLAN_CMD) self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) # Verify the backup and config state dir are gone self.assertFalse(os.path.isdir(backup)) self.assertFalse(os.path.isdir(tmpdir)) def test_netplan_dbus_config_apply(self): cid = self._new_config_object() tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid) backup = self.tmp + '/run/netplan/config-BACKUP' with open(os.path.join(tmpdir, 'etc', 'netplan', 'apply_test.yaml'), 'w') as f: f.write('TESTING-apply') with open(os.path.join(tmpdir, 'lib', 'netplan', 'apply_test.yaml'), 'w') as f: f.write('TESTING-apply') with open(os.path.join(tmpdir, 'run', 'netplan', 'apply_test.yaml'), 'w') as f: f.write('TESTING-apply') # Verify .Config.Apply() teardown of the config object and state dirs BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Apply", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) self.assertEqual(self.mock_netplan_cmd.calls(), [["netplan", "apply", "--state=%s/run/netplan/config-BACKUP" % self.tmp]]) time.sleep(1) # Give some time for 'Apply' to clean up self.assertFalse(os.path.isdir(tmpdir)) # Verify the new YAML files were copied over self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'apply_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'apply_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'apply_test.yaml'))) # Verify the object is gone from the bus err = self._check_dbus_error(BUSCTL_NETPLAN_CMD) self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) # Verify the backup and config state dir are gone self.assertFalse(os.path.isdir(backup)) self.assertFalse(os.path.isdir(tmpdir)) def test_netplan_dbus_config_try_cancel(self): # touch self._netplan_try_stamp to signal that 'netplan try' is ready # to take (Accept/Reject) input signals, before the timeout self.mock_netplan_cmd.touch(self._netplan_try_stamp) # self-terminate after 30 dsec = 3 sec, if not cancelled before self.mock_netplan_cmd.set_timeout(30) cid = self._new_config_object() tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid) backup = self.tmp + '/run/netplan/config-BACKUP' with open(os.path.join(tmpdir, 'etc', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') with open(os.path.join(tmpdir, 'lib', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') with open(os.path.join(tmpdir, 'run', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') # Verify .Config.Try() setup of the config object and state dirs BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "3", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) # Verify the temp state still exists self.assertTrue(os.path.isdir(tmpdir)) self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'etc', 'netplan', 'try_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'run', 'netplan', 'try_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'lib', 'netplan', 'try_test.yaml'))) # Verify the backup has been created self.assertTrue(os.path.isdir(backup)) self.assertTrue(os.path.isfile(os.path.join(backup, 'etc', 'netplan', 'main_test.yaml'))) # Verify the new YAML files were copied over self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'try_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'try_test.yaml'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'try_test.yaml'))) BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Cancel", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) self.assertEqual(b'b true\n', out) time.sleep(1) # Give some time for 'Cancel' to clean up # Verify the backup andconfig state dir are gone self.assertFalse(os.path.isdir(backup)) self.assertFalse(os.path.isdir(tmpdir)) # Verify the backup has been restored self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'try_test.yaml'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'try_test.yaml'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'try_test.yaml'))) # Verify the config object is gone from the bus err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) # Verify 'netplan try' has been called self.assertEqual(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=3", "--state=%s/run/netplan/config-BACKUP" % self.tmp]]) def test_netplan_dbus_config_try_cb(self): self.mock_netplan_cmd.touch(self._netplan_try_stamp) self.mock_netplan_cmd.set_timeout(1) # actually self-terminate after 0.1 sec cid = self._new_config_object() tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid) backup = self.tmp + '/run/netplan/config-BACKUP' with open(os.path.join(tmpdir, 'etc', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') with open(os.path.join(tmpdir, 'lib', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') with open(os.path.join(tmpdir, 'run', 'netplan', 'try_test.yaml'), 'w') as f: f.write('TESTING-try') BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "1", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) time.sleep(1.5) # Give some time for the callback to clean up # Verify the backup and config state dir are gone self.assertFalse(os.path.isdir(backup)) self.assertFalse(os.path.isdir(tmpdir)) # Verify the backup has been restored self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'try_test.yaml'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'try_test.yaml'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'try_test.yaml'))) # Verify the config object is gone from the bus err = self._check_dbus_error(BUSCTL_NETPLAN_CMD) self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) # Verify 'netplan try' has been called self.assertEqual(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=1", "--state=%s/run/netplan/config-BACKUP" % self.tmp]]) def test_netplan_dbus_config_try_apply(self): self.mock_netplan_cmd.touch(self._netplan_try_stamp) self.mock_netplan_cmd.set_timeout(30) # 30 dsec = 3 sec cid = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "3", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan", "io.netplan.Netplan", "Apply", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('Another \'netplan try\' process is already running', err) def test_netplan_dbus_config_try_config_try(self): self.mock_netplan_cmd.touch(self._netplan_try_stamp) self.mock_netplan_cmd.set_timeout(50) # 50 dsec = 5 sec cid = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "3", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) cid2 = self._new_config_object() BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Try", "u", "5", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('Another Try() is currently in progress: PID ', err) def test_netplan_dbus_config_try_no_ready_signal(self): # Do NOT touch the /tmp/netplan-try.rady stamp file to indicate that # 'netplan try' is not ready to take any signals self.mock_netplan_cmd.set_timeout(1) self.assertFalse(os.path.isfile(self._netplan_try_stamp)) cid = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "1", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b false\n', out) def test_netplan_dbus_config_set_invalidate(self): self.mock_netplan_cmd.set_timeout(30) # 30 dsec = 3 sec cid = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=true", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) # Calling Set() on the same config object still works BUSCTL_NETPLAN_CMD1 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=yes", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD1) self.assertEqual(b'b true\n', out) cid2 = self._new_config_object() # Calling Set() on another config object fails BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('This config was invalidated by another config object', err) # Calling Try() on another config object fails BUSCTL_NETPLAN_CMD3 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Try", "u", "3", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD3) self.assertIn('This config was invalidated by another config object', err) # Calling Apply() on another config object fails BUSCTL_NETPLAN_CMD4 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Apply", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD4) self.assertIn('This config was invalidated by another config object', err) # Calling Apply() on the same config object still works BUSCTL_NETPLAN_CMD5 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Apply", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD5) self.assertEqual(b'b true\n', out) # Verify that Set()/Apply() was only called by one config object self.assertEqual(self.mock_netplan_cmd.calls(), [ ["netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd", "--root-dir={}/run/netplan/config-{}".format(self.tmp, cid)], ["netplan", "set", "ethernets.eth0.dhcp4=yes", "--origin-hint=70-snapd", "--root-dir={}/run/netplan/config-{}".format(self.tmp, cid)], ["netplan", "apply", "--state=%s/run/netplan/config-BACKUP" % self.tmp] ]) # Now it works again cid3 = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid3), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid3), "io.netplan.Netplan.Config", "Apply", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) def test_netplan_dbus_config_set_uninvalidate(self): self.mock_netplan_cmd.set_timeout(2) cid = self._new_config_object() cid2 = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=true", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) # Calling Set() on another config object fails BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('This config was invalidated by another config object', err) # Calling Cancel() clears the dirty state BUSCTL_NETPLAN_CMD3 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Cancel", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD3) self.assertEqual(b'b true\n', out) # Calling Set() on the other config object works now out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) self.assertEqual(b'b true\n', out) # Verify the call stack self.assertEqual(self.mock_netplan_cmd.calls(), [ ["netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd", "--root-dir={}/run/netplan/config-{}".format(self.tmp, cid)], ["netplan", "set", "ethernets.eth0.dhcp4=false", "--origin-hint=70-snapd", "--root-dir={}/run/netplan/config-{}".format(self.tmp, cid2)] ]) def test_netplan_dbus_config_set_uninvalidate_timeout(self): self.mock_netplan_cmd.touch(self._netplan_try_stamp) self.mock_netplan_cmd.set_timeout(1) # actually self-terminate process after 0.1 sec cid = self._new_config_object() cid2 = self._new_config_object() BUSCTL_NETPLAN_CMD = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=true", "70-snapd", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) BUSCTL_NETPLAN_CMD1 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", "Try", "u", "1", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD1) self.assertEqual(b'b true\n', out) # Calling Set() on another config object fails BUSCTL_NETPLAN_CMD2 = [ "busctl", "call", "--system", "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid2), "io.netplan.Netplan.Config", "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", ] err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) self.assertIn('This config was invalidated by another config object', err) time.sleep(1.5) # Wait for the child process to self-terminate # Calling Set() on the other config object works now out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) self.assertEqual(b'b true\n', out) # Verify the call stack self.assertEqual(self.mock_netplan_cmd.calls(), [ ["netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd", "--root-dir={}/run/netplan/config-{}".format(self.tmp, cid)], ["netplan", "try", "--timeout=1", "--state=%s/run/netplan/config-BACKUP" % self.tmp], ["netplan", "set", "ethernets.eth0.dhcp4=false", "--origin-hint=70-snapd", "--root-dir={}/run/netplan/config-{}".format(self.tmp, cid2)] ]) netplan-1.0/tests/parser/000077500000000000000000000000001457004145200154535ustar00rootroot00000000000000netplan-1.0/tests/parser/__init__.py000066400000000000000000000012571457004145200175710ustar00rootroot00000000000000# # __init__ for parser tests. # # Copyright (C) 2021 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . netplan-1.0/tests/parser/base.py000066400000000000000000000200451457004145200167400ustar00rootroot00000000000000# # Functional tests of netplan's keyfile parser that verify that the generated # YAML files look as expected. These are run during "make check" and # don't touch the system configuration at all. # # Copyright (C) 2021 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from configparser import ConfigParser import netplan import os import re import sys import shutil import tempfile import unittest import contextlib import subprocess exe_generate = os.environ.get('NETPLAN_GENERATE_PATH', os.path.join(os.path.dirname(os.path.dirname( os.path.dirname(os.path.abspath(__file__)))), 'generate')) # make sure we point to libnetplan properly. os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))}) # make sure we fail on criticals os.environ['G_DEBUG'] = 'fatal-criticals' WOKE_REPLACE_REGEX = ' +# wokeignore:rule=[a-z]+' # A contextmanager to catch the output on a low level so that it catches output # from a subprocess or C library call, in addition to normal python output @contextlib.contextmanager def capture_stderr(): stderr_fd = 2 # 2 = stderr with tempfile.NamedTemporaryFile(mode='w+b') as tmp: stderr_copy = os.dup(stderr_fd) try: sys.stderr.flush() os.dup2(tmp.fileno(), stderr_fd) yield tmp finally: sys.stderr.flush() os.dup2(stderr_copy, stderr_fd) os.close(stderr_copy) class TestKeyfileBase(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() self.confdir = os.path.join(self.workdir.name, 'etc', 'netplan') self.maxDiff = None os.makedirs(self.confdir) def tearDown(self): shutil.rmtree(self.workdir.name) super().tearDown() def generate_from_keyfile(self, keyfile, netdef_id=None, expect_fail=False, filename=None, regenerate=True): '''Call libnetplan with given keyfile string as configuration''' # Autodetect default 'NM-' netdef-id ssid = '' keyfile = re.sub(WOKE_REPLACE_REGEX, '', keyfile) # calculate the UUID+SSID string found_values = 0 uuid = 'UNKNOWN_UUID' ssid = '' for line in keyfile.splitlines(): if line.startswith('uuid='): uuid = line.split('=')[1] found_values += 1 elif line.startswith('ssid='): ssid += '-' + line.split('=')[1] found_values += 1 if found_values >= 2: break if not netdef_id: netdef_id = 'NM-' + uuid yaml_path = os.path.join(self.workdir.name, 'etc', 'netplan', '90-NM-'+uuid+'.yaml') generated_file = 'netplan-{}{}.nmconnection'.format(netdef_id, ssid) original_file = filename or generated_file f = os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/{}'.format(original_file)) os.makedirs(os.path.dirname(f)) # Create the original keyfile that will be parsed by netplan with open(f, 'w') as file: file.write(keyfile) with capture_stderr() as outf: parser = netplan.Parser() if expect_fail: try: parser.load_keyfile(f) except netplan.NetplanException as err: return err.message else: ret = parser.load_keyfile(f) # Throws netplan.NetplanExcption on failure self.assertTrue(ret) # If the original file does not have a standard netplan-*.nmconnection # filename it is being deleted in favor of the newly generated file. # It has been parsed and is not needed anymore in this case if generated_file != original_file: os.remove(f) state = netplan.State() state.import_parser_results(parser) with open(yaml_path, 'w') as f: os.chmod(yaml_path, mode=0o600) state._dump_yaml(f) # check re-generated keyfile if regenerate: self.assert_nm_regenerate({generated_file: keyfile}) with open(outf.name, 'r') as f: output = f.read().strip() # output from stderr (fd=2) on C/library level return output def assert_netplan(self, file_contents_map): for uuid in file_contents_map.keys(): file_contents_map[uuid] = re.sub(WOKE_REPLACE_REGEX, '', file_contents_map[uuid]) path = os.path.join(self.confdir, '90-NM-{}.yaml'.format(uuid)) self.assertTrue(os.path.isfile(path)) st = os.stat(path) permission = oct(st.st_mode & 0o777) self.assertEqual(permission, '0o600') with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(uuid)), 'r') as f: self.assertEqual(f.read(), file_contents_map[uuid]) def normalize_keyfile(self, file_contents): parser = ConfigParser() parser.read_string(file_contents) sections = parser.sections() res = [] # Sort sections and keys sections.sort() for s in sections: items = parser.items(s) if s == 'ipv6' and len(items) == 1 and items[0] == ('method', 'ignore'): continue line = '\n[' + s + ']' res.append(line) items.sort(key=lambda tup: tup[0]) for k, v in items: # Normalize lines if k == 'addr-gen-mode': v = v.replace('1', 'stable-privacy').replace('0', 'eui64') elif k == 'ip6-privacy' and v == '0': continue elif k == 'wake-on-lan' and v == '1': continue elif k == 'stp' and v == 'true': continue elif k.startswith('route'): v = v.replace(',::', ',').replace(',0.0.0.0', ',') v = v.strip(',') line = (k + '=' + v).strip(';') res.append(line) return '\n'.join(res).strip()+'\n' def assert_nm_regenerate(self, file_contents_map): argv = [exe_generate, '--root-dir', self.workdir.name] p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) returncode = p.wait(5) (out, err) = p.communicate() self.assertEqual(returncode, 0, err) self.assertEqual(out, '') con_dir = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'system-connections') if file_contents_map: self.assertEqual(set(os.listdir(con_dir)), set([n for n in file_contents_map])) for fname, contents in file_contents_map.items(): contents = re.sub(WOKE_REPLACE_REGEX, '', contents) with open(os.path.join(con_dir, fname)) as f: generated_keyfile = self.normalize_keyfile(f.read()) normalized_contents = self.normalize_keyfile(contents) self.assertEqual(generated_keyfile, normalized_contents, 'Re-generated keyfile does not match') else: # pragma: nocover (only needed for test debugging) if os.path.exists(con_dir): self.assertEqual(os.listdir(con_dir), []) return err netplan-1.0/tests/parser/test_keyfile.py000066400000000000000000001471361457004145200205300ustar00rootroot00000000000000#!/usr/bin/python3 # Functional tests of NetworkManager keyfile parser. These are run during # "make check" and don't touch the system configuration at all. # # Copyright (C) 2021 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import ctypes import ctypes.util import netplan from .base import TestKeyfileBase rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) exe_cli = os.path.join(rootdir, 'src', 'netplan.script') lib = ctypes.CDLL('libnetplan.so.1') UUID = 'ff9d6ebc-226d-4f82-a485-b7ff83b9607f' class TestNetworkManagerKeyfileParser(TestKeyfileBase): '''Test NM keyfile parser as used by NetworkManager's YAML backend''' def test_keyfile_missing_uuid(self): err = self.generate_from_keyfile('[connection]\ntype=ethernets', expect_fail=True) self.assertIn('netplan: Keyfile: cannot find connection.uuid', err) def test_keyfile_missing_type(self): err = self.generate_from_keyfile('[connection]\nuuid=87749f1d-334f-40b2-98d4-55db58965f5f', expect_fail=True) self.assertIn('netplan: Keyfile: cannot find connection.type', err) def test_keyfile_gsm(self): self.generate_from_keyfile('''[connection] id=T-Mobile Funkadelic 2 uuid={} type=gsm [gsm] apn=internet2.voicestream.com device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 home-only=true network-id=254098 password=parliament2 pin=123456 sim-id=89148000000060671234 sim-operator-id=310260 username=george.clinton.again mtu=1042 [ipv4] dns-search= method=auto [ipv6] dns-search= method=auto ip6-privacy=0 '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 modems: NM-{}: renderer: NetworkManager match: {{}} dhcp4: true dhcp6: true mtu: 1042 apn: "internet2.voicestream.com" device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" network-id: "254098" pin: "123456" sim-id: "89148000000060671234" sim-operator-id: "310260" username: "george.clinton.again" password: "parliament2" networkmanager: uuid: "{}" name: "T-Mobile Funkadelic 2" passthrough: gsm.home-only: "true" ipv4.dns-search: "" ipv6.dns-search: "" '''.format(UUID, UUID)}) def test_keyfile_cdma(self): self.generate_from_keyfile('''[connection] id=T-Mobile Funkadelic 2 uuid={} type=cdma [cdma] number=0123456 username=testuser password=testpass mtu=1042 [ipv4] method=auto [ipv6] method=ignore '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 modems: NM-{}: renderer: NetworkManager match: {{}} dhcp4: true mtu: 1042 username: "testuser" password: "testpass" number: "0123456" networkmanager: uuid: "{}" name: "T-Mobile Funkadelic 2" '''.format(UUID, UUID)}) def test_keyfile_gsm_via_bluetooth(self): self.generate_from_keyfile('''[connection] id=T-Mobile Funkadelic 2 uuid={} type=bluetooth [gsm] apn=internet2.voicestream.com device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 home-only=true network-id=254098 password=parliament2 pin=123456 sim-id=89148000000060671234 sim-operator-id=310260 username=george.clinton.again [ipv4] dns-search= method=auto [ipv6] dns-search= method=auto [proxy]'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 nm-devices: NM-{}: renderer: NetworkManager networkmanager: uuid: "{}" name: "T-Mobile Funkadelic 2" passthrough: connection.type: "bluetooth" gsm.apn: "internet2.voicestream.com" gsm.device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" gsm.home-only: "true" gsm.network-id: "254098" gsm.password: "parliament2" gsm.pin: "123456" gsm.sim-id: "89148000000060671234" gsm.sim-operator-id: "310260" gsm.username: "george.clinton.again" ipv4.dns-search: "" ipv4.method: "auto" ipv6.dns-search: "" ipv6.method: "auto" proxy._: "" '''.format(UUID, UUID)}) def test_keyfile_method_auto(self): self.generate_from_keyfile('''[connection] id=Test uuid={} type=ethernet [ethernet] wake-on-lan=0 mtu=1500 cloned-mac-address=00:11:22:33:44:55 [ipv4] dns-search= method=auto ignore-auto-routes=true never-default=true route-metric=4242 [ipv6] addr-gen-mode=eui64 dns-search= method=auto ip6-privacy=0 ignore-auto-routes=true never-default=true route-metric=4242 [proxy] '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: {{}} dhcp4: true dhcp4-overrides: use-routes: false route-metric: 4242 dhcp6: true dhcp6-overrides: use-routes: false route-metric: 4242 macaddress: "00:11:22:33:44:55" ipv6-address-generation: "eui64" mtu: 1500 networkmanager: uuid: "{}" name: "Test" passthrough: ipv4.dns-search: "" ipv6.dns-search: "" proxy._: "" '''.format(UUID, UUID)}) def test_keyfile_fail_validation(self): err = self.generate_from_keyfile('''[connection] id=Test uuid={} type=ethernet [ethernet] wake-on-lan=0 [ipv4] method=auto [ipv6] addr-gen-mode=eui64 token=::42 method=auto '''.format(UUID), expect_fail=True) self.assertIn('Error in network definition:', err) def test_keyfile_method_manual(self): self.generate_from_keyfile('''[connection] id=Test uuid={} type=ethernet [ethernet] mac-address=00:11:22:33:44:55 [ipv4] dns-search=foo.local;bar.remote; dns=9.8.7.6;5.4.3.2 method=manual address1=1.2.3.4/24,8.8.8.8 address2=5.6.7.8/16 gateway=6.6.6.6 route1=1.1.2.2/16,8.8.8.8,42 route1_options=onlink=true,initrwnd=33,initcwnd=44,mtu=1024,table=102,src=10.10.10.11 route2=2.2.3.3/24,4.4.4.4 [ipv6] addr-gen-mode=stable-privacy dns-search=bar.local dns=dead:beef::2; method=manual ip6-privacy=2 address1=1:2:3::9/128 gateway=6:6::6 route1=dead:beef::1/128,2001:1234::2 route1_options=unknown=invalid, route2=4:5:6:7:8:9:0:1/63,,5 [proxy] '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: macaddress: "00:11:22:33:44:55" addresses: - "1.2.3.4/24" - "5.6.7.8/16" - "1:2:3::9/128" nameservers: addresses: - 9.8.7.6 - 5.4.3.2 - dead:beef::2 gateway4: 6.6.6.6 gateway6: 6:6::6 ipv6-address-generation: "stable-privacy" ipv6-privacy: true routes: - metric: 42 table: 102 mtu: 1024 congestion-window: 44 advertised-receive-window: 33 on-link: true from: "10.10.10.11" to: "1.1.2.2/16" via: "8.8.8.8" - to: "2.2.3.3/24" via: "4.4.4.4" - to: "dead:beef::1/128" via: "2001:1234::2" - scope: "link" metric: 5 to: "4:5:6:7:8:9:0:1/63" wakeonlan: true networkmanager: uuid: "{}" name: "Test" passthrough: ipv4.dns-search: "foo.local;bar.remote;" ipv4.method: "manual" ipv4.address1: "1.2.3.4/24,8.8.8.8" ipv6.dns-search: "bar.local" ipv6.route1: "dead:beef::1/128,2001:1234::2" ipv6.route1_options: "unknown=invalid," proxy._: "" '''.format(UUID, UUID)}) def test_keyfile_dummy(self): # wokeignore:rule=dummy self.generate_from_keyfile('''[connection] id=Test uuid={} type={} [ipv4] method=manual address1=192.168.123.123/24 '''.format(UUID, 'dummy')) # wokeignore:rule=dummy self.assert_netplan({UUID: '''network: version: 2 dummy-devices: # wokeignore:rule=dummy NM-{}: renderer: NetworkManager addresses: - "192.168.123.123/24" networkmanager: uuid: "{}" name: "Test" '''.format(UUID, UUID)}) def _template_keyfile_type(self, nd_type, nm_type): self.maxDiff = None extra = '' file = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(file)) with open(file, 'w') as f: f.write('[connection]\ntype={}\nuuid={}'.format(nm_type, UUID)) if nm_type == 'ip-tunnel': f.write('\n\n[ip-tunnel]\nmode=4\nremote=10.0.0.1') extra = '\n mode: "isatap"\n remote: "10.0.0.1"' if nd_type in ['ethernets', 'modems', 'wifis']: extra = '\n match: {}' parser = netplan.Parser() state = netplan.State() parser.load_keyfile(file) state.import_parser_results(parser) output_file = '90-NM-{}.yaml'.format(UUID) state._write_yaml_file(output_file, self.workdir.name) self.assertTrue(os.path.isfile(os.path.join(self.confdir, output_file))) with open(os.path.join(self.confdir, output_file), 'r') as f: self.assertEqual(f.read(), '''network: version: 2 {}: NM-{}: renderer: NetworkManager{} networkmanager: uuid: "{}" '''.format(nd_type, UUID, extra, UUID)) def test_keyfile_ethernet(self): self._template_keyfile_type('ethernets', 'ethernet') def test_keyfile_type_modem_gsm(self): self._template_keyfile_type('modems', 'gsm') def test_keyfile_type_modem_cdma(self): self._template_keyfile_type('modems', 'cdma') def test_keyfile_type_bridge(self): self._template_keyfile_type('bridges', 'bridge') def test_keyfile_type_bond(self): self._template_keyfile_type('bonds', 'bond') def test_keyfile_type_tunnel(self): self._template_keyfile_type('tunnels', 'ip-tunnel') def test_keyfile_type_wifi(self): self.generate_from_keyfile('''[connection] type=wifi uuid={} permissions= id=myid with spaces interface-name=eth0 [wifi] ssid=SOME-SSID mode=infrastructure hidden=true mtu=1500 cloned-mac-address=00:11:22:33:44:55 band=a channel=12 bssid=de:ad:be:ef:ca:fe [wifi-security] key-mgmt=ieee8021x [ipv4] method=auto dns-search='''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "eth0" dhcp4: true macaddress: "00:11:22:33:44:55" mtu: 1500 access-points: "SOME-SSID": hidden: true bssid: "de:ad:be:ef:ca:fe" band: "5GHz" channel: 12 auth: key-management: "802.1x" networkmanager: uuid: "{}" name: "myid with spaces" passthrough: connection.permissions: "" ipv4.dns-search: "" networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, UUID, UUID)}) def _template_keyfile_type_wifi_eap(self, method): self.generate_from_keyfile('''[connection] type=wifi uuid={} permissions= id=testnet interface-name=wlan0 [wifi] ssid=testnet mode=infrastructure [wifi-security] key-mgmt=wpa-eap [802-1x] eap={} identity=some-id anonymous-identity=anon-id password=v3rys3cr3t! ca-cert=/some/path.key client-cert=/some/path.client_cert private-key=/some/path.key private-key-password=s0s3cr3t!!111 phase2-auth=chap [ipv4] method=auto dns-search='''.format(UUID, method)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "wlan0" dhcp4: true access-points: "testnet": auth: key-management: "eap" method: "{}" anonymous-identity: "anon-id" identity: "some-id" ca-certificate: "/some/path.key" client-certificate: "/some/path.client_cert" client-key: "/some/path.key" client-key-password: "s0s3cr3t!!111" phase2-auth: "chap" password: "v3rys3cr3t!" networkmanager: uuid: "{}" name: "testnet" passthrough: connection.permissions: "" ipv4.dns-search: "" networkmanager: uuid: "{}" name: "testnet" '''.format(UUID, method, UUID, UUID)}) def test_keyfile_type_wifi_eap_peap(self): self._template_keyfile_type_wifi_eap('peap') def test_keyfile_type_wifi_eap_tls(self): self._template_keyfile_type_wifi_eap('tls') def test_keyfile_type_wifi_eap_ttls(self): self._template_keyfile_type_wifi_eap('ttls') def test_keyfile_type_wifi_eap_leap(self): self._template_keyfile_type_wifi_eap('leap') def test_keyfile_type_wifi_eap_pwd(self): self._template_keyfile_type_wifi_eap('pwd') def test_keyfile_wifi_eap_leap(self): self.generate_from_keyfile('''[connection] type=wifi uuid={} permissions= id=myid with spaces interface-name=eth0 [wifi] ssid=SOME-SSID mode=infrastructure [wifi-security] key-mgmt=ieee8021x [802-1x] eap=leap identity=some-id password=v3rys3cr3t! [ipv4] method=auto'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "eth0" dhcp4: true access-points: "SOME-SSID": auth: key-management: "802.1x" method: "leap" identity: "some-id" password: "v3rys3cr3t!" networkmanager: uuid: "{}" name: "myid with spaces" passthrough: connection.permissions: "" networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, UUID, UUID)}) def test_keyfile_wifi_eap_pwd(self): self.generate_from_keyfile('''[connection] type=wifi uuid={} permissions= id=myid with spaces interface-name=eth0 [wifi] ssid=SOME-SSID mode=infrastructure [wifi-security] key-mgmt=ieee8021x [802-1x] eap=pwd identity=some-id password=v3rys3cr3t! [ipv4] method=auto'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "eth0" dhcp4: true access-points: "SOME-SSID": auth: key-management: "802.1x" method: "pwd" identity: "some-id" password: "v3rys3cr3t!" networkmanager: uuid: "{}" name: "myid with spaces" passthrough: connection.permissions: "" networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, UUID, UUID)}) def test_keyfile_wifi_eap_md5_not_supported(self): self.generate_from_keyfile('''[connection] type=wifi uuid={} permissions= id=myid with spaces interface-name=eth0 [wifi] ssid=SOME-SSID mode=infrastructure [wifi-security] key-mgmt=ieee8021x [802-1x] eap=md5 identity=some-id password=v3rys3cr3t! [ipv4] method=auto'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "eth0" dhcp4: true access-points: "SOME-SSID": auth: key-management: "802.1x" identity: "some-id" password: "v3rys3cr3t!" networkmanager: uuid: "{}" name: "myid with spaces" passthrough: connection.permissions: "" 802-1x.eap: "md5" networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, UUID, UUID)}) def test_keyfile_wifi_eap_psk_with_eap(self): self.generate_from_keyfile('''[connection] type=wifi uuid={} permissions= id=myid with spaces interface-name=eth0 [wifi] ssid=SOME-SSID mode=infrastructure [wifi-security] key-mgmt=wpa-eap psk=pskpassword [802-1x] eap=leap identity=some-id password=v3rys3cr3t! [ipv4] method=auto'''.format(UUID), regenerate=False) # Regeneration is disabled for the previous test because when both, # identity and PSK passwords are used, the PSK will be stored in the # access-points..password key and, because of that, a keyfile with # [wifi-security].pmf=2 will be emitted and will differ from the original # keyfile. self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "eth0" dhcp4: true access-points: "SOME-SSID": password: "pskpassword" auth: key-management: "eap" method: "leap" identity: "some-id" password: "v3rys3cr3t!" networkmanager: uuid: "{}" name: "myid with spaces" passthrough: connection.permissions: "" networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, UUID, UUID)}) def _template_keyfile_type_wifi(self, nd_mode, nm_mode): self.generate_from_keyfile('''[connection] type=wifi uuid={} id=myid with spaces [ipv4] method=auto [wifi] ssid=SOME-SSID wake-on-wlan=24 band=bg mode={}'''.format(UUID, nm_mode)) wifi_mode = '' ap_mode = '' if nm_mode != nd_mode: wifi_mode = ''' passthrough: wifi.mode: "{}"'''.format(nm_mode) if nd_mode != 'infrastructure': ap_mode = '\n mode: "%s"' % nd_mode self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: {{}} dhcp4: true wakeonwlan: - magic_pkt - gtk_rekey_failure access-points: "SOME-SSID": band: "2.4GHz"{} networkmanager: uuid: "{}" name: "myid with spaces"{} networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, ap_mode, UUID, wifi_mode, UUID)}) def test_keyfile_type_wifi_ap(self): self.generate_from_keyfile('''[connection] type=wifi uuid={} id=myid with spaces [ipv4] method=shared [wifi] ssid=SOME-SSID wake-on-wlan=24 band=bg mode=ap'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: {{}} wakeonwlan: - magic_pkt - gtk_rekey_failure access-points: "SOME-SSID": band: "2.4GHz" mode: "ap" networkmanager: uuid: "{}" name: "myid with spaces" networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, UUID, UUID)}) def test_keyfile_type_wifi_adhoc(self): self._template_keyfile_type_wifi('adhoc', 'adhoc') def test_keyfile_type_wifi_unknown(self): self._template_keyfile_type_wifi('infrastructure', 'mesh') def test_keyfile_type_wifi_missing_ssid(self): err = self.generate_from_keyfile('''[connection]\ntype=wifi\nuuid={}\nid=myid with spaces''' .format(UUID), expect_fail=True) self.assertFalse(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) self.assertIn('netplan: Keyfile: cannot find SSID for WiFi connection', err) def test_keyfile_wake_on_lan(self): self.generate_from_keyfile('''[connection] type=ethernet uuid={} id=myid with spaces [ethernet] wake-on-lan=2 [ipv4] method=auto'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: {{}} dhcp4: true wakeonlan: true networkmanager: uuid: "{}" name: "myid with spaces" passthrough: ethernet.wake-on-lan: "2" '''.format(UUID, UUID)}) def test_keyfile_wake_on_lan_nm_default(self): self.generate_from_keyfile('''[connection] type=ethernet uuid={} id=myid with spaces [ethernet] wake-on-lan=0 [ipv4] method=auto'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: {{}} dhcp4: true networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, UUID)}) def test_keyfile_modem_gsm(self): self.generate_from_keyfile('''[connection] type=gsm uuid={} id=myid with spaces [ipv4] method=auto [gsm] auto-config=true'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 modems: NM-{}: renderer: NetworkManager match: {{}} dhcp4: true auto-config: true networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, UUID)}) def test_keyfile_existing_id(self): self.generate_from_keyfile('''[connection] type=bridge interface-name=mybr uuid={} id=renamed netplan bridge [ipv4] method=auto'''.format(UUID), netdef_id='mybr') self.assert_netplan({UUID: '''network: version: 2 bridges: mybr: renderer: NetworkManager dhcp4: true networkmanager: uuid: "{}" name: "renamed netplan bridge" '''.format(UUID)}) def test_keyfile_yaml_wifi_hotspot(self): self.generate_from_keyfile('''[connection] id=Hotspot-1 type=wifi uuid={} interface-name=wlan0 autoconnect=false permissions= [ipv4] method=shared dns-search= [ipv6] method=ignore addr-gen-mode=1 dns-search= ip6-privacy=0 [wifi] ssid=my-hotspot mode=ap mac-address-blacklist= # wokeignore:rule=blacklist [wifi-security] group=ccmp; key-mgmt=wpa-psk pairwise=ccmp; proto=rsn; psk=test1234 [proxy]'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "wlan0" access-points: "my-hotspot": auth: key-management: "psk" password: "test1234" mode: "ap" networkmanager: uuid: "{}" name: "Hotspot-1" passthrough: connection.autoconnect: "false" connection.permissions: "" ipv4.dns-search: "" ipv6.addr-gen-mode: "1" ipv6.dns-search: "" wifi.mac-address-blacklist: "" # wokeignore:rule=blacklist wifi-security.group: "ccmp;" wifi-security.pairwise: "ccmp;" wifi-security.proto: "rsn;" proxy._: "" networkmanager: uuid: "{}" name: "Hotspot-1" '''.format(UUID, UUID, UUID)}) def test_keyfile_ip4_linklocal_ip6_ignore(self): self.generate_from_keyfile('''[connection] id=netplan-eth1 type=ethernet interface-name=eth1 uuid={} [ethernet] wake-on-lan=0 [ipv4] method=link-local [ipv6] method=ignore '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: name: "eth1" networkmanager: uuid: "{}" name: "netplan-eth1" '''.format(UUID, UUID)}) def test_keyfile_vlan(self): self.generate_from_keyfile('''[connection] id=netplan-enblue type=vlan interface-name=enblue uuid={} [vlan] id=1 parent=en1 [ipv4] method=manual address1=1.2.3.4/24 [ipv6] method=ignore '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 vlans: NM-{}: renderer: NetworkManager addresses: - "1.2.3.4/24" id: 1 link: "en1" networkmanager: uuid: "{}" name: "netplan-enblue" passthrough: connection.interface-name: "enblue" '''.format(UUID, UUID)}) def test_keyfile_bridge(self): self.generate_from_keyfile('''[connection] id=netplan-br0 type=bridge interface-name=br0 uuid={} [bridge] ageing-time=50 priority=1000 forward-delay=12 hello-time=6 max-age=24 stp=false [ipv4] method=auto [ipv6] method=ignore '''.format(UUID), netdef_id='br0', expect_fail=False, filename="netplan-br0.nmconnection") self.assert_netplan({UUID: '''network: version: 2 bridges: br0: renderer: NetworkManager dhcp4: true parameters: ageing-time: "50" forward-delay: "12" hello-time: "6" max-age: "24" priority: 1000 stp: false networkmanager: uuid: "{}" name: "netplan-br0" '''.format(UUID)}) def test_keyfile_bridge_default_stp(self): self.generate_from_keyfile('''[connection] id=netplan-br0 type=bridge interface-name=br0 uuid={} [bridge] hello-time=6 [ipv4] method=auto [ipv6] method=ignore '''.format(UUID), netdef_id='br0') self.assert_netplan({UUID: '''network: version: 2 bridges: br0: renderer: NetworkManager dhcp4: true parameters: hello-time: "6" networkmanager: uuid: "{}" name: "netplan-br0" '''.format(UUID)}) def test_keyfile_bond(self): self.generate_from_keyfile('''[connection] uuid={} id=netplan-bn0 type=bond interface-name=bn0 [bond] mode=802.3ad lacp_rate=fast miimon=10 min_links=10 xmit_hash_policy=none ad_select=none all_slaves_active=1 # wokeignore:rule=slave arp_interval=10 arp_ip_target=10.10.10.10,20.20.20.20 arp_validate=all arp_all_targets=all updelay=10 downdelay=10 fail_over_mac=none num_grat_arp=10 num_unsol_na=10 packets_per_slave=10 # wokeignore:rule=slave primary_reselect=none resend_igmp=10 lp_interval=10 [ipv4] method=auto [ipv6] method=ignore '''.format(UUID), netdef_id='bn0', expect_fail=False, filename='some.keyfile') self.assert_netplan({UUID: '''network: version: 2 bonds: bn0: renderer: NetworkManager dhcp4: true parameters: mode: "802.3ad" mii-monitor-interval: "10" up-delay: "10" down-delay: "10" lacp-rate: "fast" transmit-hash-policy: "none" ad-select: "none" arp-validate: "all" arp-all-targets: "all" fail-over-mac-policy: "none" primary-reselect-policy: "none" learn-packet-interval: "10" arp-interval: "10" min-links: 10 all-members-active: true gratuitous-arp: 10 packets-per-member: 10 resend-igmp: 10 arp-ip-targets: - 10.10.10.10 - 20.20.20.20 networkmanager: uuid: "{}" name: "netplan-bn0" '''.format(UUID)}) def test_keyfile_customer_A1(self): self.generate_from_keyfile('''[connection] id=netplan-wlan0-TESTSSID type=wifi interface-name=wlan0 uuid={} [ipv4] method=auto [ipv6] method=ignore [wifi] ssid=TESTSSID mode=infrastructure [wifi-security] key-mgmt=wpa-psk psk=s0s3cr1t '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "wlan0" dhcp4: true access-points: "TESTSSID": auth: key-management: "psk" password: "s0s3cr1t" networkmanager: uuid: "ff9d6ebc-226d-4f82-a485-b7ff83b9607f" name: "netplan-wlan0-TESTSSID" networkmanager: uuid: "{}" name: "netplan-wlan0-TESTSSID" '''.format(UUID, UUID)}) def test_keyfile_customer_A2(self): self.generate_from_keyfile('''[connection] id=gsm type=gsm uuid={} interface-name=cdc-wdm1 [gsm] apn=internet [ipv4] method=auto address1=10.10.28.159/24 address2=10.10.164.254/24 address3=10.10.246.132/24 dns=8.8.8.8;8.8.4.4; [ipv6] method=auto addr-gen-mode=1 ip6-privacy=0 '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 modems: NM-{}: renderer: NetworkManager match: name: "cdc-wdm1" nameservers: addresses: - 8.8.8.8 - 8.8.4.4 dhcp4: true dhcp6: true apn: "internet" networkmanager: uuid: "{}" name: "gsm" passthrough: ipv4.address1: "10.10.28.159/24" ipv4.address2: "10.10.164.254/24" ipv4.address3: "10.10.246.132/24" ipv6.addr-gen-mode: "1" '''.format(UUID, UUID)}) def test_keyfile_netplan0103_compat(self): self.generate_from_keyfile('''[connection] id=Work Wired uuid={} type=ethernet autoconnect=false permissions= timestamp=305419896 [ethernet] mac-address=99:88:77:66:55:44 mac-address-blacklist= # wokeignore:rule=blacklist mtu=900 [ipv4] address1=192.168.0.5/24,192.168.0.1 address2=1.2.3.4/8 dns=4.2.2.1;4.2.2.2; dns-search= method=manual route1=10.10.10.2/24,10.10.10.1,3 route2=1.1.1.1/8,1.2.1.1,1 route3=2.2.2.2/7 route4=3.3.3.3/6,0.0.0.0,4 route4_options=cwnd=10,mtu=1492,src=1.2.3.4 [ipv6] addr-gen-mode=stable-privacy address1=abcd::beef/64 address2=dcba::beef/56 dns=1::cafe;2::cafe; dns-search=wallaceandgromit.com; method=manual ip6-privacy=1 route1=1:2:3:4:5:6:7:8/64,8:7:6:5:4:3:2:1,3 route2=2001::1000/56,2001::1111,1 route3=4:5:6:7:8:9:0:1/63,::,5 route4=5:6:7:8:9:0:1:2/62 [proxy] '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: macaddress: "99:88:77:66:55:44" addresses: - "192.168.0.5/24" - "1.2.3.4/8" - "abcd::beef/64" - "dcba::beef/56" nameservers: addresses: - 4.2.2.1 - 4.2.2.2 - 1::cafe - 2::cafe ipv6-address-generation: "stable-privacy" mtu: 900 routes: - metric: 3 to: "10.10.10.2/24" via: "10.10.10.1" - metric: 1 to: "1.1.1.1/8" via: "1.2.1.1" - scope: "link" to: "2.2.2.2/7" - scope: "link" metric: 4 mtu: 1492 from: "1.2.3.4" to: "3.3.3.3/6" - metric: 3 to: "1:2:3:4:5:6:7:8/64" via: "8:7:6:5:4:3:2:1" - metric: 1 to: "2001::1000/56" via: "2001::1111" - scope: "link" metric: 5 to: "4:5:6:7:8:9:0:1/63" - scope: "link" to: "5:6:7:8:9:0:1:2/62" wakeonlan: true networkmanager: uuid: "{}" name: "Work Wired" passthrough: connection.autoconnect: "false" connection.permissions: "" connection.timestamp: "305419896" ethernet.mac-address-blacklist: "" # wokeignore:rule=blacklist ipv4.address1: "192.168.0.5/24,192.168.0.1" ipv4.dns-search: "" ipv4.method: "manual" ipv4.route4: "3.3.3.3/6,0.0.0.0,4" ipv4.route4_options: "cwnd=10,mtu=1492,src=1.2.3.4" ipv6.dns-search: "wallaceandgromit.com;" ipv6.ip6-privacy: "1" proxy._: "" '''.format(UUID, UUID)}) def test_keyfile_tunnel_regression_lp1952967(self): self.generate_from_keyfile('''[connection] id=IP tunnel connection 1 uuid={} type=ip-tunnel autoconnect=false interface-name=gre10 permissions= [ip-tunnel] local=10.20.20.1 mode=2 remote=10.20.20.2 [ipv4] dns-search= method=auto [ipv6] addr-gen-mode=stable-privacy dns-search= method=auto [proxy] '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true dhcp6: true ipv6-address-generation: "stable-privacy" mode: "gre" local: "10.20.20.1" remote: "10.20.20.2" networkmanager: uuid: "{}" name: "IP tunnel connection 1" passthrough: connection.autoconnect: "false" connection.interface-name: "gre10" connection.permissions: "" ipv4.dns-search: "" ipv6.dns-search: "" ipv6.ip6-privacy: "-1" proxy._: "" '''.format(UUID, UUID)}) def test_keyfile_ip6_privacy_default_netplan_0104_compat(self): self.generate_from_keyfile('''[connection] id=Test uuid={} type=ethernet [ethernet] mac-address=99:88:77:66:55:44 [ipv4] method=auto [ipv6] method=auto '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: macaddress: "99:88:77:66:55:44" dhcp4: true dhcp6: true wakeonlan: true networkmanager: uuid: "{}" name: "Test" passthrough: ipv6.ip6-privacy: "-1" '''.format(UUID, UUID)}) def test_keyfile_wpa3_sae(self): self.generate_from_keyfile('''[connection] id=test2 uuid={} type=wifi interface-name=wlan0 [wifi] mode=infrastructure ssid=ubuntu-wpa2-wpa3-mixed [wifi-security] key-mgmt=sae psk=test1234 pmf=3 [ipv4] method=auto [ipv6] addr-gen-mode=stable-privacy method=auto [proxy] '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "wlan0" dhcp4: true dhcp6: true ipv6-address-generation: "stable-privacy" access-points: "ubuntu-wpa2-wpa3-mixed": auth: key-management: "sae" password: "test1234" networkmanager: uuid: "ff9d6ebc-226d-4f82-a485-b7ff83b9607f" name: "test2" passthrough: ipv6.ip6-privacy: "-1" proxy._: "" networkmanager: uuid: "{}" name: "test2" '''.format(UUID, UUID)}) def test_keyfile_wpa3_enterprise_eap_sha256(self): self.generate_from_keyfile('''[connection] id=test2 uuid={} type=wifi interface-name=wlan0 [wifi] mode=infrastructure ssid=enterprisenet [wifi-security] key-mgmt=wpa-eap pmf=2 [802-1x] eap=tls identity=cert-joe@cust.example.com anonymous-identity=@cust.example.com ca-cert=/etc/ssl/cust-cacrt.pem client-cert=/etc/ssl/cust-crt.pem private-key=/etc/ssl/cust-key.pem private-key-password=********** [ipv4] method=auto [ipv6] addr-gen-mode=stable-privacy method=auto [proxy] '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "wlan0" dhcp4: true dhcp6: true ipv6-address-generation: "stable-privacy" access-points: "enterprisenet": auth: key-management: "eap-sha256" method: "tls" anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: "/etc/ssl/cust-cacrt.pem" client-certificate: "/etc/ssl/cust-crt.pem" client-key: "/etc/ssl/cust-key.pem" client-key-password: "**********" networkmanager: uuid: "ff9d6ebc-226d-4f82-a485-b7ff83b9607f" name: "test2" passthrough: ipv6.ip6-privacy: "-1" proxy._: "" networkmanager: uuid: "{}" name: "test2" '''.format(UUID, UUID)}) def test_keyfile_wpa3_enterprise_eap_suite_b_192(self): self.generate_from_keyfile('''[connection] id=test2 uuid={} type=wifi interface-name=wlan0 [wifi] mode=infrastructure ssid=enterprisenet [wifi-security] key-mgmt=wpa-eap-suite-b-192 pmf=3 [802-1x] eap=tls identity=cert-joe@cust.example.com anonymous-identity=@cust.example.com ca-cert=/etc/ssl/cust-cacrt.pem client-cert=/etc/ssl/cust-crt.pem private-key=/etc/ssl/cust-key.pem private-key-password=********** [ipv4] method=auto [ipv6] addr-gen-mode=stable-privacy method=auto [proxy] '''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "wlan0" dhcp4: true dhcp6: true ipv6-address-generation: "stable-privacy" access-points: "enterprisenet": auth: key-management: "eap-suite-b-192" method: "tls" anonymous-identity: "@cust.example.com" identity: "cert-joe@cust.example.com" ca-certificate: "/etc/ssl/cust-cacrt.pem" client-certificate: "/etc/ssl/cust-crt.pem" client-key: "/etc/ssl/cust-key.pem" client-key-password: "**********" networkmanager: uuid: "ff9d6ebc-226d-4f82-a485-b7ff83b9607f" name: "test2" passthrough: ipv6.ip6-privacy: "-1" proxy._: "" networkmanager: uuid: "{}" name: "test2" '''.format(UUID, UUID)}) def test_keyfile_dns_search_ip4_ip6_conflict(self): self.generate_from_keyfile('''[connection] id=Work Wired type=ethernet uuid={} autoconnect=false timestamp=305419896 [ethernet] wake-on-lan=1 mac-address=99:88:77:66:55:44 mtu=900 [ipv4] method=manual address1=192.168.0.5/24,192.168.0.1 address2=1.2.3.4/8 dns=4.2.2.1;4.2.2.2; [ipv6] method=manual address1=abcd::beef/64 address2=dcba::beef/56 addr-gen-mode=1 dns=1::cafe;2::cafe; dns-search=wallaceandgromit.com; [proxy]\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: macaddress: "99:88:77:66:55:44" addresses: - "192.168.0.5/24" - "1.2.3.4/8" - "abcd::beef/64" - "dcba::beef/56" nameservers: addresses: - 4.2.2.1 - 4.2.2.2 - 1::cafe - 2::cafe mtu: 900 wakeonlan: true networkmanager: uuid: "{}" name: "Work Wired" passthrough: connection.autoconnect: "false" connection.timestamp: "305419896" ethernet.wake-on-lan: "1" ipv4.method: "manual" ipv4.address1: "192.168.0.5/24,192.168.0.1" ipv6.addr-gen-mode: "1" ipv6.dns-search: "wallaceandgromit.com;" ipv6.ip6-privacy: "-1" proxy._: "" '''.format(UUID, UUID)}) def test_keyfile_nm_140_default_ethernet_group(self): self.generate_from_keyfile('''[connection] id=Test Write Bridge Main uuid={} type=bridge interface-name=br0 [ethernet] [bridge] [ipv4] address1=1.2.3.4/24,1.1.1.1 method=manual [ipv6] addr-gen-mode=default method=auto [proxy]\n'''.format(UUID), netdef_id='br0') self.assert_netplan({UUID: '''network: version: 2 bridges: br0: renderer: NetworkManager addresses: - "1.2.3.4/24" dhcp6: true networkmanager: uuid: "{}" name: "Test Write Bridge Main" passthrough: ethernet._: "" bridge._: "" ipv4.address1: "1.2.3.4/24,1.1.1.1" ipv4.method: "manual" ipv6.addr-gen-mode: "default" ipv6.ip6-privacy: "-1" proxy._: "" '''.format(UUID)}) def test_multiple_eap_methods(self): self.generate_from_keyfile('''[connection] id=MyWifi uuid={} type=wifi interface-name=wlp2s0 [wifi] mode=infrastructure ssid=MyWifi [wifi-security] auth-alg=open key-mgmt=wpa-eap [802-1x] ca-cert=/path/to/my/crt.crt eap=peap;tls identity=username password=123456 phase2-auth=mschapv2 [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "wlp2s0" dhcp4: true access-points: "MyWifi": auth: key-management: "eap" method: "peap" identity: "username" ca-certificate: "/path/to/my/crt.crt" phase2-auth: "mschapv2" password: "123456" networkmanager: uuid: "{}" name: "MyWifi" passthrough: wifi-security.auth-alg: "open" 802-1x.eap: "peap;tls" networkmanager: uuid: "{}" name: "MyWifi" '''.format(UUID, UUID, UUID)}) def test_single_eap_method(self): self.generate_from_keyfile('''[connection] id=MyWifi uuid={} type=wifi interface-name=wlp2s0 [wifi] mode=infrastructure ssid=MyWifi [wifi-security] auth-alg=open key-mgmt=wpa-eap [802-1x] ca-cert=/path/to/my/crt.crt eap=peap; identity=username password=123456 phase2-auth=mschapv2 [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "wlp2s0" dhcp4: true access-points: "MyWifi": auth: key-management: "eap" method: "peap" identity: "username" ca-certificate: "/path/to/my/crt.crt" phase2-auth: "mschapv2" password: "123456" networkmanager: uuid: "{}" name: "MyWifi" passthrough: wifi-security.auth-alg: "open" networkmanager: uuid: "{}" name: "MyWifi" '''.format(UUID, UUID, UUID)}) def test_simple_wireguard(self): self.generate_from_keyfile('''[connection] id=wg0 type=wireguard uuid={} interface-name=wg0 [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "wireguard" networkmanager: uuid: "{}" name: "wg0" passthrough: connection.interface-name: "wg0" '''.format(UUID, UUID)}) def test_wireguard_with_key(self): self.generate_from_keyfile('''[connection] id=wg0 type=wireguard uuid={} interface-name=wg0 [wireguard] private-key=aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A= [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "wireguard" keys: private: "aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A=" networkmanager: uuid: "{}" name: "wg0" passthrough: connection.interface-name: "wg0" '''.format(UUID, UUID)}) def test_wireguard_with_key_and_peer(self): self.generate_from_keyfile('''[connection] id=wg0 type=wireguard uuid={} interface-name=wg0 [wireguard] private-key=aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A= [wireguard-peer.cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI=] endpoint=1.2.3.4:12345 allowed-ips=192.168.0.0/24; [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "wireguard" keys: private: "aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A=" peers: - endpoint: "1.2.3.4:12345" keys: public: "cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI=" allowed-ips: - "192.168.0.0/24" networkmanager: uuid: "{}" name: "wg0" passthrough: connection.interface-name: "wg0" '''.format(UUID, UUID)}) def test_wireguard_with_empty_endpoint(self): self.generate_from_keyfile('''[connection] id=wg0 type=wireguard uuid={} interface-name=wg0 [wireguard] private-key=aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A= [wireguard-peer.cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI=] endpoint= allowed-ips=192.168.0.0/24; [ipv4] method=auto\n'''.format(UUID), regenerate=False) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "wireguard" keys: private: "aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A=" peers: - keys: public: "cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI=" allowed-ips: - "192.168.0.0/24" networkmanager: uuid: "{}" name: "wg0" passthrough: connection.interface-name: "wg0" '''.format(UUID, UUID)}) def test_wireguard_allowed_ips_without_prefix(self): ''' When the IP prefix is not present we should default to /32 for IPv4 and /128 for IPv6. ''' self.generate_from_keyfile('''[connection] id=wg0 type=wireguard uuid={} interface-name=wg0 [wireguard] private-key=aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A= [wireguard-peer.cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI=] endpoint=1.2.3.4:12345 allowed-ips=192.168.0.10;2001::1; [ipv4] method=auto\n'''.format(UUID), regenerate=False) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "wireguard" keys: private: "aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A=" peers: - endpoint: "1.2.3.4:12345" keys: public: "cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI=" allowed-ips: - "192.168.0.10/32" - "2001::1/128" networkmanager: uuid: "{}" name: "wg0" passthrough: connection.interface-name: "wg0" '''.format(UUID, UUID)}) def test_wireguard_with_key_flags(self): self.generate_from_keyfile('''[connection] id=wg0 type=wireguard uuid={} interface-name=wg0 [wireguard] listen-port=51820 private-key-flags=1 [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] endpoint=10.20.30.40:51820 allowed-ips=0.0.0.0/0; [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "wireguard" port: 51820 keys: private-key-flags: - agent-owned peers: - endpoint: "10.20.30.40:51820" keys: public: "M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=" allowed-ips: - "0.0.0.0/0" networkmanager: uuid: "{}" name: "wg0" passthrough: connection.interface-name: "wg0" '''.format(UUID, UUID)}) def test_wireguard_with_key_all_flags_enabled(self): self.generate_from_keyfile('''[connection] id=wg0 type=wireguard uuid={} interface-name=wg0 [wireguard] listen-port=51820 private-key-flags=7 [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] endpoint=10.20.30.40:51820 allowed-ips=0.0.0.0/0; [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "wireguard" port: 51820 keys: private-key-flags: - agent-owned - not-saved - not-required peers: - endpoint: "10.20.30.40:51820" keys: public: "M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=" allowed-ips: - "0.0.0.0/0" networkmanager: uuid: "{}" name: "wg0" passthrough: connection.interface-name: "wg0" '''.format(UUID, UUID)}) def test_wireguard_with_key_and_peer_without_allowed_ips(self): self.generate_from_keyfile('''[connection] id=wg0 type=wireguard uuid={} interface-name=wg0 [wireguard] private-key=aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A= [wireguard-peer.cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI=] endpoint=1.2.3.4:12345 [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "wireguard" keys: private: "aPUcp5vHz8yMLrzk8SsDyYnV33IhE/k20e52iKJFV0A=" peers: - endpoint: "1.2.3.4:12345" keys: public: "cwkb7k0xDgLSnunZpFIjLJw4u+mJDDr+aBR5DqzpmgI=" networkmanager: uuid: "{}" name: "wg0" passthrough: connection.interface-name: "wg0" '''.format(UUID, UUID)}) def test_vxlan_with_local_and_remote(self): self.generate_from_keyfile('''[connection] id=vxlan10 type=vxlan uuid={} interface-name=vxlan10 [vxlan] id=10 local=198.51.100.2 remote=203.0.113.1 [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "vxlan" local: "198.51.100.2" remote: "203.0.113.1" id: 10 networkmanager: uuid: "{}" name: "vxlan10" passthrough: connection.interface-name: "vxlan10" '''.format(UUID, UUID)}) def test_simple_vxlan(self): self.generate_from_keyfile('''[connection] id=vxlan10 type=vxlan uuid={} interface-name=vxlan10 [vxlan] id=10 [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 tunnels: NM-{}: renderer: NetworkManager dhcp4: true mode: "vxlan" id: 10 networkmanager: uuid: "{}" name: "vxlan10" passthrough: connection.interface-name: "vxlan10" '''.format(UUID, UUID)}) def test_invalid_tunnel_mode(self): out = self.generate_from_keyfile('''[connection] id=tun0 type=ip-tunnel uuid={} interface-name=tun0 [ip-tunnel] mode=42 [ipv4] method=auto\n'''.format(UUID), expect_fail=True) self.assertIn('missing or invalid \'mode\' property for tunnel', out) def test_keyfile_wifi_random_cloned_mac_address(self): self.generate_from_keyfile('''[connection] type=wifi uuid={} id=myid with spaces interface-name=eth0 [wifi] ssid=SOME-SSID mode=infrastructure cloned-mac-address=random [wifi-security] key-mgmt=ieee8021x [ipv4] method=auto dns-search='''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 wifis: NM-{}: renderer: NetworkManager match: name: "eth0" dhcp4: true macaddress: "random" access-points: "SOME-SSID": auth: key-management: "802.1x" networkmanager: uuid: "{}" name: "myid with spaces" passthrough: ipv4.dns-search: "" networkmanager: uuid: "{}" name: "myid with spaces" '''.format(UUID, UUID, UUID)}) def test_keyfile_ethernet_random_cloned_mac_address(self): self.generate_from_keyfile('''[connection] type=ethernet uuid={} id=myid with spaces interface-name=eth0 [ethernet] cloned-mac-address=random [ipv4] method=auto dns-search='''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 ethernets: NM-{}: renderer: NetworkManager match: name: "eth0" dhcp4: true macaddress: "random" wakeonlan: true networkmanager: uuid: "{}" name: "myid with spaces" passthrough: ipv4.dns-search: "" '''.format(UUID, UUID)}) def test_veth_pair(self): self.generate_from_keyfile('''[connection] id=veth-peer1 uuid={} type=veth interface-name=veth-peer1 [veth] peer=veth-peer2 [ipv4] method=auto\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 virtual-ethernets: NM-{}: renderer: NetworkManager dhcp4: true peer: "veth-peer2" networkmanager: uuid: "{}" name: "veth-peer1" passthrough: connection.interface-name: "veth-peer1" '''.format(UUID, UUID)}) def test_veth_without_peer(self): self.generate_from_keyfile('''[connection] id=veth-peer1 uuid={} type=veth interface-name=veth-peer1 [ipv4] method=auto\n'''.format(UUID), expect_fail=True) def test_vrf_basic(self): self.generate_from_keyfile('''[connection] id=vrf0 uuid={} type=vrf interface-name=vrf0 [vrf] table=1000 [ipv4] route1=10.10.0.0/16,10.10.10.1 route1_options=table=1000 method=link-local\n'''.format(UUID)) self.assert_netplan({UUID: '''network: version: 2 vrfs: NM-{}: renderer: NetworkManager routes: - to: "10.10.0.0/16" via: "10.10.10.1" table: 1000 networkmanager: uuid: "{}" name: "vrf0" passthrough: connection.interface-name: "vrf0" '''.format(UUID, UUID)}) def test_vrf_without_table_should_fail(self): out = self.generate_from_keyfile('''[connection] id=vrf0 uuid={} type=vrf interface-name=vrf0 [vrf] [ipv4] route1=10.10.0.0/16,10.10.10.1 route1_options=table=1000 method=link-local\n'''.format(UUID), expect_fail=True) self.assertIn('missing \'table\' property', out) netplan-1.0/tests/spread/000077500000000000000000000000001457004145200154355ustar00rootroot00000000000000netplan-1.0/tests/spread/dbus/000077500000000000000000000000001457004145200163725ustar00rootroot00000000000000netplan-1.0/tests/spread/dbus/task.yaml000066400000000000000000000022401457004145200202160ustar00rootroot00000000000000summary: Check that the dbus integration works debug: | netplan get execute: | # see the basics work netplan get bridges.br54.dhcp4 | MATCH true # TODO: actually use busctl obj_path=$(dbus-send --system --print-reply --type=method_call \ --dest=io.netplan.Netplan /io/netplan/Netplan \ io.netplan.Netplan.Config|tail -1 | sed 's/.*"\(.*\)".*/\1/' ) dbus-send --print-reply --system --type=method_call \ --dest=io.netplan.Netplan "$obj_path" \ io.netplan.Netplan.Config.Get | MATCH "version: 2" # and updating via dbus netplan works (use 90-test origin hint for # easier test cleanup) dbus-send --print-reply --system --type=method_call \ --dest=io.netplan.Netplan "$obj_path" \ io.netplan.Netplan.Config.Set \ string:bridges.br54.dhcp4=false string:90-test # not applied yet test ! -e /etc/netplan/90-test.yaml netplan get bridges.br54.dhcp4 | MATCH true # apply dbus-send --print-reply --system --type=method_call \ --dest=io.netplan.Netplan "$obj_path" \ io.netplan.Netplan.Config.Apply # and now it's active test -e /etc/netplan/90-test.yaml netplan get bridges.br54.dhcp4 | MATCH false netplan-1.0/tests/spread/snapd-integration/000077500000000000000000000000001457004145200210635ustar00rootroot00000000000000netplan-1.0/tests/spread/snapd-integration/task.yaml000066400000000000000000000032611457004145200227130ustar00rootroot00000000000000summary: Check that the snapd/netplan integration works prepare: | # put snapd in debug mode for good logs mkdir -p /etc/systemd/system/snapd.service.d cat < /etc/systemd/system/snapd.service.d/local.conf [Service] Environment=SNAPD_DEBUG=1 EOF systemctl daemon-reload # install snapd if needed (should be installed on most lxd images) if ! dpkg -l snapd >/dev/null; then apt install -y snapd echo "apt autoremove -y snapd" >> defered.cleanup fi # fake running on core (netplan only enabled on ubuntu core) # FIXME: find a more offical way, this if fugly cp /etc/os-release /etc/os-release.save sed -i s/ID=ubuntu/ID=ubuntu-core/ /etc/os-release M=/var/lib/snapd/assertions/asserts-v0/model/16/generic/generic-classic/active cp "$M" model.save sed -i 's/classic: true/architecture: amd64\ngadget: pc\nkernel: pc-kernel/g' "$M" # restart snapd systemctl restart snapd restore: | mv /etc/os-release.save /etc/os-release mv model.save /var/lib/snapd/assertions/asserts-v0/model/16/generic/generic-classic/active sudo systemctl restart snapd if [ -e defered.cleanup ]; then sh -ex defered.cleanup fi rm -f model.* debug: | journalctl -u snapd snap get system -d snap version execute: | # see the basics work netplan get bridges.br54.dhcp4 | MATCH true snap get system system.network.netplan.network.bridges.br54.dhcp4 | MATCH true # and updating netplan works snap set system system.network.netplan.network.bridges.br54.dhcp4=false netplan get bridges.br54.dhcp4 | MATCH false snap get system system.network.netplan.network.bridges.br54.dhcp4 | MATCH false netplan-1.0/tests/test_configmanager.py000066400000000000000000000320111457004145200203650ustar00rootroot00000000000000#!/usr/bin/python3 # Validate ConfigManager methods # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import shutil import tempfile import unittest from netplan_cli.configmanager import ConfigManager, ConfigurationError from netplan_cli.cli.ovs import OPENVSWITCH_OVS_VSCTL class TestConfigManager(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() self.configmanager = ConfigManager(prefix=self.workdir.name, extra_files={}) os.makedirs(os.path.join(self.workdir.name, "etc/netplan")) os.makedirs(os.path.join(self.workdir.name, "run/systemd/network")) os.makedirs(os.path.join(self.workdir.name, "run/NetworkManager/system-connections")) with open(os.path.join(self.workdir.name, "newfile.yaml"), 'w') as fd: print('''network: version: 2 ethernets: ethtest: dhcp4: yes ''', file=fd) with open(os.path.join(self.workdir.name, "newfile_merging.yaml"), 'w') as fd: print('''network: version: 2 ethernets: eth0: dhcp6: on eth42: dhcp4: on ethbr1: dhcp4: on ''', file=fd) with open(os.path.join(self.workdir.name, "newfile_emptydict.yaml"), 'w') as fd: print('''network: version: 2 ethernets: eth0: {} bridges: br666: {} ''', file=fd) with open(os.path.join(self.workdir.name, "ovs_merging.yaml"), 'w') as fd: print('''network: version: 2 openvswitch: ports: [[patchx, patchc], [patchy, patchd]] bridges: ovs0: {openvswitch: {}} ''', file=fd) with open(os.path.join(self.workdir.name, "invalid.yaml"), 'w') as fd: print('''network: version: 2 vlans: vlan78: id: 78 link: ethinvalid ''', file=fd) with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd openvswitch: ports: [[patcha, patchb]] other-config: disable-in-band: true ethernets: lo: addresses: [ 192.168.10.10/32 ] eth0: dhcp4: false ethbr1: dhcp4: false ethbr2: dhcp4: false ethbond1: dhcp4: false ethbond2: dhcp4: false wifis: wlan1: access-points: testAP: {} modems: wwan0: apn: internet pin: 1234 dhcp4: yes addresses: [1.2.3.4/24, 5.6.7.8/24] vlans: vlan2: id: 2 link: eth0 tunnels: he-ipv6: mode: sit remote: 2.2.2.2 local: 1.1.1.1 addresses: - "2001:dead:beef::2/64" gateway6: "2001:dead:beef::1" vxlan1005: mode: vxlan id: 1005 link: lo mtu: 8950 accept-ra: no neigh-suppress: true mac-learning: false port: 4789 local: 192.168.10.10 remote: 1.1.1.1 vxlan1: mode: vxlan id: 1 link: lo mtu: 8950 accept-ra: no neigh-suppress: true mac-learning: false port: 4789 local: 192.168.10.10 remote: 1.1.1.1 bridges: br3: interfaces: [ ethbr1, vxlan1005 ] br4: interfaces: [ ethbr2, vxlan1 ] parameters: stp: on vrfs: vrf1005: table: 1005 interfaces: - br3 - br4 vrf1006: table: 1006 interfaces: [] bonds: bond5: interfaces: [ ethbond1 ] bond6: interfaces: [ ethbond2 ] parameters: mode: 802.3ad nm-devices: fallback: renderer: NetworkManager networkmanager: passthrough: connection.id: some-nm-id connection.uuid: some-uuid connection.type: ethernet ''', file=fd) with open(os.path.join(self.workdir.name, "etc/netplan/test2.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd dummy-devices: dm0: addresses: - 192.168.0.123/24 virtual-ethernets: veth0-peer1: peer: veth0-peer2 veth0-peer2: peer: veth0-peer1 ''', file=fd) with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'w') as fd: print("pretend .network", file=fd) with open(os.path.join(self.workdir.name, "run/NetworkManager/system-connections/pretend"), 'w') as fd: print("pretend NM config", file=fd) def test_parse(self): self.configmanager.parse() state = self.configmanager.np_state assert state self.assertIn('lo', self.configmanager.ethernets) self.assertIn('eth0', state.ethernets) self.assertIn('bond6', state.bonds) self.assertIn('eth0', self.configmanager.physical_interfaces) self.assertNotIn('bond7', self.configmanager.netdefs) self.assertNotIn('bond6', self.configmanager.physical_interfaces) self.assertIn('wwan0', state.modems) self.assertIn('wwan0', self.configmanager.physical_interfaces) # self.assertIn('apn', self.configmanager.modems.get('wwan0')) self.assertIn('he-ipv6', state.tunnels) self.assertNotIn('he-ipv6', self.configmanager.physical_interfaces) # self.assertIn('remote', self.configmanager.tunnels.get('he-ipv6')) self.assertIn('patcha', state.ovs_ports) self.assertIn('patchb', state.ovs_ports) self.assertEqual('networkd', state.backend) self.assertIn('fallback', state.nm_devices) self.assertIn('vrf1005', self.configmanager.virtual_interfaces) self.assertIn('vlan2', self.configmanager.virtual_interfaces) self.assertIn('br3', self.configmanager.virtual_interfaces) self.assertIn('br4', self.configmanager.virtual_interfaces) self.assertIn('veth0-peer1', self.configmanager.virtual_interfaces) self.assertIn('veth0-peer2', self.configmanager.virtual_interfaces) self.assertIn('dm0', self.configmanager.virtual_interfaces) self.assertIn('vxlan1005', self.configmanager.virtual_interfaces) self.assertIn('vxlan1', self.configmanager.virtual_interfaces) self.assertIn('bond5', self.configmanager.virtual_interfaces) self.assertIn('bond6', self.configmanager.virtual_interfaces) self.assertIn('he-ipv6', self.configmanager.virtual_interfaces) def test_parse_merging(self): state = self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile_merging.yaml")]) self.assertIn('eth0', state.ethernets) self.assertIn('eth42', state.ethernets) @unittest.skipIf(not os.path.exists(OPENVSWITCH_OVS_VSCTL), 'OpenVSwitch not installed') def test_parse_merging_ovs(self): state = self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "ovs_merging.yaml")]) self.assertIn('eth0', state.ethernets) # self.assertIn('dhcp4', state.ethernets['eth0']) self.assertIn('patchx', state.ovs_ports) self.assertIn('patchy', state.ovs_ports) self.assertIn('ovs0', state.bridges) self.assertEqual('OpenVSwitch', state['ovs0'].backend) self.assertEqual('OpenVSwitch', state['patchx'].backend) self.assertEqual('OpenVSwitch', state['patchy'].backend) def test_parse_emptydict(self): state = self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile_emptydict.yaml")]) self.assertIn('br666', state.bridges) self.assertIn('eth0', state.ethernets) def test_parse_invalid(self): with self.assertRaises(ConfigurationError): self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "invalid.yaml")]) def test_parse_extra_config(self): state = self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile.yaml")]) self.assertIn('ethtest', state.ethernets) self.assertIn('bond6', state.bonds) def test_add(self): self.configmanager.add({os.path.join(self.workdir.name, "newfile.yaml"): os.path.join(self.workdir.name, "etc/netplan/newfile.yaml")}) self.assertIn(os.path.join(self.workdir.name, "newfile.yaml"), self.configmanager.extra_files) self.assertTrue(os.path.exists(os.path.join(self.workdir.name, "etc/netplan/newfile.yaml"))) def test_backup_missing_dirs(self): backup_dir = self.configmanager.tempdir shutil.rmtree(os.path.join(self.workdir.name, "run/systemd/network")) self.configmanager.backup(backup_config_dir=False) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/NetworkManager/system-connections/pretend"))) # no source dir means no backup as well self.assertFalse(os.path.exists(os.path.join(backup_dir, "run/systemd/network/01-pretend.network"))) self.assertFalse(os.path.exists(os.path.join(backup_dir, "etc/netplan/test.yaml"))) def test_backup_without_config_file(self): backup_dir = self.configmanager.tempdir self.configmanager.backup(backup_config_dir=False) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/NetworkManager/system-connections/pretend"))) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/systemd/network/01-pretend.network"))) self.assertFalse(os.path.exists(os.path.join(backup_dir, "etc/netplan/test.yaml"))) def test_backup_with_config_file(self): backup_dir = self.configmanager.tempdir self.configmanager.backup(backup_config_dir=True) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/NetworkManager/system-connections/pretend"))) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/systemd/network/01-pretend.network"))) self.assertTrue(os.path.exists(os.path.join(backup_dir, "etc/netplan/test.yaml"))) def test_revert(self): self.configmanager.backup() with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'a+') as fd: print("CHANGED", file=fd) with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'r') as fd: lines = fd.readlines() self.assertIn("CHANGED\n", lines) self.configmanager.revert() with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'r') as fd: lines = fd.readlines() self.assertNotIn("CHANGED\n", lines) def test_revert_extra_files(self): self.configmanager.add({os.path.join(self.workdir.name, "newfile.yaml"): os.path.join(self.workdir.name, "etc/netplan/newfile.yaml")}) self.assertIn(os.path.join(self.workdir.name, "newfile.yaml"), self.configmanager.extra_files) self.assertTrue(os.path.exists(os.path.join(self.workdir.name, "etc/netplan/newfile.yaml"))) self.configmanager.revert() self.assertNotIn(os.path.join(self.workdir.name, "newfile.yaml"), self.configmanager.extra_files) self.assertFalse(os.path.exists(os.path.join(self.workdir.name, "etc/netplan/newfile.yaml"))) def test_cleanup(self): backup_dir = self.configmanager.tempdir self.assertTrue(os.path.exists(backup_dir)) self.configmanager.cleanup() self.assertFalse(os.path.exists(backup_dir)) def test_destruction(self): backup_dir = self.configmanager.tempdir self.assertTrue(os.path.exists(backup_dir)) del self.configmanager self.assertFalse(os.path.exists(backup_dir)) def test_cleanup_and_destruction(self): backup_dir = self.configmanager.tempdir self.assertTrue(os.path.exists(backup_dir)) self.configmanager.cleanup() self.assertFalse(os.path.exists(backup_dir)) # This tests that the rmtree in the destructor does not throw an error # if cleanup was already called del self.configmanager self.assertFalse(os.path.exists(backup_dir)) def test__copy_tree(self): self.configmanager._copy_tree(os.path.join(self.workdir.name, "etc"), os.path.join(self.workdir.name, "etc2")) self.assertTrue(os.path.exists(os.path.join(self.workdir.name, "etc2/netplan/test.yaml"))) def test__copy_tree_missing_source(self): with self.assertRaises(FileNotFoundError): self.configmanager._copy_tree(os.path.join(self.workdir.name, "nonexistent"), os.path.join(self.workdir.name, "nonexistent2"), missing_ok=False) netplan-1.0/tests/test_libnetplan.py000066400000000000000000001033511457004145200177230ustar00rootroot00000000000000#!/usr/bin/python3 # Functional tests of certain libnetplan functions. These are run during # "make check" and don't touch the system configuration at all. # # Copyright (C) 2020-2021 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import ctypes import os import shutil import tempfile import unittest import io import yaml from generator.base import TestBase from parser.base import capture_stderr from utils import state_from_yaml from netplan_cli.cli.commands.set import FALLBACK_FILENAME from netplan_cli.cli.ovs import OPENVSWITCH_OVS_VSCTL import netplan from netplan.netdef import NetplanRoute # We still need direct (ctypes) access to libnetplan.so to test certain cases # that are not covered by the 'netplan' module bindings lib = ctypes.CDLL('libnetplan.so.1') rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) exe_cli = os.path.join(rootdir, 'src', 'netplan.script') class TestRawLibnetplan(TestBase): '''Test libnetplan functionality as used by the NetworkManager backend''' def setUp(self): super().setUp() os.makedirs(self.confdir) def tearDown(self): shutil.rmtree(self.workdir.name) super().tearDown() def test_parse_keyfile_missing(self): parser = netplan.Parser() f = os.path.join(self.workdir.name, 'tmp/some.keyfile') os.makedirs(os.path.dirname(f)) with self.assertRaises(netplan.NetplanException) as ctx: parser.load_keyfile(f) self.assertIn('No such file or directory', str(ctx.exception)) def test_delete_connection(self): os.environ["TEST_NETPLAN_CMD"] = exe_cli orig = os.path.join(self.confdir, 'some-filename.yaml') with open(orig, 'w') as f: f.write('''network: ethernets: some-netplan-id: dhcp4: true''') self.assertTrue(os.path.isfile(orig)) # Parse all YAML and delete 'some-netplan-id' connection file self.assertTrue(lib.netplan_delete_connection('some-netplan-id'.encode(), self.workdir.name.encode())) self.assertFalse(os.path.isfile(orig)) self.assertFalse(os.path.isfile(os.path.join(self.confdir, FALLBACK_FILENAME))) def test_delete_connection_id_not_found(self): orig = os.path.join(self.confdir, 'some-filename.yaml') with open(orig, 'w') as f: f.write('''network: ethernets: some-netplan-id: dhcp4: true''') self.assertTrue(os.path.isfile(orig)) with capture_stderr() as outf: self.assertFalse(lib.netplan_delete_connection('unknown-id'.encode(), self.workdir.name.encode())) self.assertTrue(os.path.isfile(orig)) with open(outf.name, 'r') as f: self.assertIn('netplan_delete_connection: Cannot delete unknown-id, does not exist.', f.read().strip()) def test_delete_connection_two_in_file(self): os.environ["TEST_NETPLAN_CMD"] = exe_cli orig = os.path.join(self.confdir, 'some-filename.yaml') with open(orig, 'w') as f: f.write('''network: ethernets: some-netplan-id: dhcp4: true other-id: dhcp6: true''') self.assertTrue(os.path.isfile(orig)) self.assertTrue(lib.netplan_delete_connection('some-netplan-id'.encode(), self.workdir.name.encode())) self.assertTrue(os.path.isfile(orig)) # Verify the file still exists and still contains the other connection with open(orig, 'r') as f: self.assertEqual(f.read(), 'network:\n version: 2\n ethernets:\n other-id:\n dhcp6: true\n') def test_delete_connection_invalid(self): orig = os.path.join(self.confdir, 'some-filename.yaml') with open(orig, 'w') as f: f.write('INVALID') self.assertTrue(os.path.isfile(orig)) with capture_stderr() as outf: self.assertFalse(lib.netplan_delete_connection('some-netplan-id'.encode(), self.workdir.name.encode())) with open(outf.name, 'r') as f: self.assertIn('Cannot parse input', f.read()) class TestNetdefIterator(TestBase): def test_with_empty_netplan(self): state = netplan.State() self.assertSequenceEqual(list(netplan.netdef.NetDefinitionIterator(state, "ethernets")), []) def test_iter_all_types(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false bridges: br0: dhcp4: false''') self.assertSetEqual(set(["eth0", "br0"]), set(d.id for d in netplan.netdef.NetDefinitionIterator(state, None))) def test_iter_all_types_with_placeholder(self): state = state_from_yaml(self.confdir, '''network: renderer: NetworkManager ethernets: eth0: dhcp4: false virtual-ethernets: # Netplan will create a placeholder netdef for veth321 veth123: peer: veth321 bridges: br0: dhcp4: false''') # We call the property "type" here so it will try to translate the netdef type to a string # and crash if it's a placeholder expected = {"ethernets", "virtual-ethernets", "bridges"} self.assertSetEqual(expected, set(d.type for d in netplan.netdef.NetDefinitionIterator(state, None))) def test_iter_ethernets(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false eth1: dhcp4: false bridges: br0: dhcp4: false''') self.assertSetEqual(set(["eth0", "eth1"]), set(d.id for d in netplan.netdef.NetDefinitionIterator(state, "ethernets"))) class TestNetdefAddressesIterator(TestBase): def test_with_empty_ip_addresses(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: true''') netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) self.assertSetEqual(set(), set(ip for ip in netdef.addresses)) def test_iter_ethernets(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: addresses: - 192.168.0.1/24 - 172.16.0.1/24 - 1234:4321:abcd::cdef/96 - abcd::1234/64''') expected = set(["1234:4321:abcd::cdef/96", "abcd::1234/64", "192.168.0.1/24", "172.16.0.1/24"]) netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) self.assertSetEqual(expected, set(ip.address for ip in netdef.addresses)) self.assertSetEqual(expected, set(str(ip) for ip in netdef.addresses)) def test_iter_ethernets_with_options(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: addresses: - 192.168.0.1/24 - 172.16.0.1/24: lifetime: 0 label: label1 - 1234:4321:abcd::cdef/96: lifetime: forever label: label2''') expected_ips = set(["1234:4321:abcd::cdef/96", "192.168.0.1/24", "172.16.0.1/24"]) expected_lifetime_options = set([None, "0", "forever"]) expected_label_options = set([None, "label1", "label2"]) netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) self.assertSetEqual(expected_ips, set(ip.address for ip in netdef.addresses)) self.assertSetEqual(expected_lifetime_options, set(ip.lifetime for ip in netdef.addresses)) self.assertSetEqual(expected_label_options, set(ip.label for ip in netdef.addresses)) def test_drop_iterator_before_finishing(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: addresses: - 192.168.0.1/24 - 1234:4321:abcd::cdef/96''') netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) iter = netdef.addresses.__iter__() address = next(iter) self.assertEqual(address.address, "192.168.0.1/24") del iter class TestNetdefNameserverSearchDomainIterator(TestBase): def test_with_empty_nameservers(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: {}''') netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) self.assertSetEqual(set(), set(ip for ip in netdef.nameserver_addresses)) self.assertSetEqual(set(), set(ip for ip in netdef.nameserver_search)) def test_iter_ethernets_nameservers_and_domains(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: nameservers: search: - home.local - mynet.local addresses: - 192.168.0.1 - 172.16.0.1 - 1234:4321:abcd::cdef - abcd::1234''') expected_addresses = set(["1234:4321:abcd::cdef", "abcd::1234", "192.168.0.1", "172.16.0.1"]) expected_domains = set(["home.local", "mynet.local"]) netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) self.assertSetEqual(expected_addresses, set(ip for ip in netdef.nameserver_addresses)) self.assertSetEqual(expected_domains, set(domain for domain in netdef.nameserver_search)) class TestNetdefRouteIterator(TestBase): def test_with_empty_routes(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: {}''') netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) self.assertTrue(len([ip for ip in netdef.routes]) == 0) def test_iter_routes(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: routes: - to: default via: 192.168.0.1 - to: 1.2.3.0/24 via: 10.20.30.40 metric: 1000 table: 1000 from: 192.168.0.0/24 - to: 3.2.1.0/24 via: 10.20.30.40 metric: 1000 table: 1000 on-link: true type: local scope: host mtu: 1500 congestion-window: 123 advertised-receive-window: 321 from: 192.168.0.0/24''') netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) routes = [route for route in netdef.routes] self.assertSetEqual({routes[0].to, routes[0].via}, {'default', '192.168.0.1'}) self.assertSetEqual({routes[1].to, routes[1].via, routes[1].metric, routes[1].table, routes[1].from_addr}, {'1.2.3.0/24', '10.20.30.40', 1000, 1000, '192.168.0.0/24'}) self.assertSetEqual({routes[2].to, routes[2].via, routes[2].metric, routes[2].table, routes[2].from_addr, routes[2].onlink, routes[2].type, routes[2].scope, routes[2].mtubytes, routes[2].congestion_window, routes[2].advertised_receive_window}, {'3.2.1.0/24', '10.20.30.40', 1000, 1000, True, 'local', 'host', 1500, 123, 321, '192.168.0.0/24'}) class TestRoute(TestBase): def test_route_str(self): route1 = {} route1['to'] = 'default' route1['via'] = '192.168.0.1' route1['from_addr'] = '192.168.0.1' route1['metric'] = 1000 route = NetplanRoute(**route1) expected_str = 'default via 192.168.0.1 type unicast scope global src 192.168.0.1 metric 1000' self.assertEqual(str(route), expected_str) def test_route_str_with_table(self): route1 = {} route1['to'] = 'default' route1['via'] = '192.168.0.1' route1['from_addr'] = '192.168.0.1' route1['metric'] = 1000 route1['table'] = 1234 route = NetplanRoute(**route1) expected_str = 'default via 192.168.0.1 type unicast scope global src 192.168.0.1 metric 1000 table 1234' self.assertEqual(str(route), expected_str) def test_routes_to_dict(self): route1 = {} route1['to'] = 'default' route1['via'] = '192.168.0.1' route1['from_addr'] = '192.168.0.1' route1['metric'] = 1000 route1['table'] = 1234 route1['family'] = 2 route = NetplanRoute(**route1) expected_dict = { 'from': '192.168.0.1', 'metric': 1000, 'table': 1234, 'to': 'default', 'type': 'unicast', 'via': '192.168.0.1', 'family': 2, } self.assertDictEqual(route.to_dict(), expected_dict) class TestParser(TestBase): def test_load_yaml_from_fd_empty(self): parser = netplan.Parser() # We just don't want it to raise an exception with tempfile.TemporaryFile() as f: parser.load_yaml(f) def test_load_yaml_from_fd_bad_yaml(self): parser = netplan.Parser() with tempfile.TemporaryFile() as f: f.write(b'invalid: {]') f.seek(0, io.SEEK_SET) with self.assertRaises(netplan.NetplanParserException) as context: parser.load_yaml(f) self.assertIn('Invalid YAML', str(context.exception)) def test_load_keyfile(self): parser = netplan.Parser() state = netplan.State() with tempfile.NamedTemporaryFile() as f: f.write(b'''[connection] id=Bridge connection 1 type=bridge uuid=990548be-01ed-42d7-9f9f-cd4966b25c08 interface-name=bridge0 [ipv4] method=auto [ipv6] method=auto addr-gen-mode=1''') f.seek(0, io.SEEK_SET) parser.load_keyfile(f.name) state.import_parser_results(parser) output = io.StringIO() state._dump_yaml(output) yaml_data = yaml.safe_load(output.getvalue()) self.assertIsNotNone(yaml_data.get('network')) class TestState(TestBase): def test_get_netdef(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''') netdef = state['eth0'] self.assertEqual("eth0", netdef.id) def test_get_netdef_empty_state(self): state = netplan.State() with self.assertRaises(IndexError): state['eth0'] def test_get_netdef_wrong_id(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''') with self.assertRaises(IndexError): state['eth1'] def test_get_netdefs_size(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''') self.assertEqual(1, len(state)) def test_bad_state(self): state = netplan.State() parser = netplan.Parser() with tempfile.NamedTemporaryFile() as f: f.write(b'''network: renderer: networkd tunnels: tun0: mode: ipip local: 10.10.10.10 remote: 20.20.20.20 keys: input: 1234 ''') f.flush() parser.load_yaml(f.name) with self.assertRaises(netplan.NetplanException): state.import_parser_results(parser) def test_dump_yaml_bad_file_perms(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''') bad_file = os.path.join(self.workdir.name, 'bad.yml') open(bad_file, 'a').close() os.chmod(bad_file, 0o444) with self.assertRaises(netplan.NetplanFileException) as context: with open(bad_file) as f: state._dump_yaml(f) self.assertIn('Invalid argument', str(context.exception)) self.assertEqual(context.exception.error, context.exception.errno) def test_dump_yaml_empty_state(self): state = netplan.State() with tempfile.TemporaryFile() as f: state._dump_yaml(f) f.flush() self.assertEqual(0, f.seek(0, io.SEEK_END)) def test_write_yaml_file_unremovable_target(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''', filename='target.yml') target = os.path.join(self.confdir, 'target.yml') os.remove(target) os.makedirs(target) with self.assertRaises(netplan.NetplanFileException): state._write_yaml_file('target.yml', self.workdir.name) def test_update_yaml_hierarchy_no_confdir(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''') shutil.rmtree(self.confdir) with self.assertRaises(netplan.NetplanFileException) as context: state._update_yaml_hierarchy("bogus", self.workdir.name) self.assertIn('No such file or directory', str(context.exception)) def test_write_yaml_file_remove_directory(self): state = netplan.State() os.makedirs(self.confdir) with tempfile.TemporaryDirectory(dir=self.confdir) as tmpdir: hint = os.path.basename(tmpdir) with self.assertRaises(netplan.NetplanFileException): state._write_yaml_file(hint, self.workdir.name) def test_write_yaml_file_file_no_confdir(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''', filename='test.yml') shutil.rmtree(self.confdir) with self.assertRaises(netplan.NetplanFileException) as context: state._write_yaml_file('test.yml', self.workdir.name) self.assertIn('No such file or directory', str(context.exception)) class TestNetDefinition(TestBase): def test_type(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''') self.assertEqual(state['eth0'].type, 'ethernets') def test_backend(self): state = state_from_yaml(self.confdir, '''network: renderer: networkd ethernets: eth0: dhcp4: false eth1: renderer: NetworkManager''') self.assertEqual(state['eth0'].backend, 'networkd') self.assertEqual(state['eth1'].backend, 'NetworkManager') def test_critical(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: critical: true eth1: {}''') self.assertTrue(state['eth0'].critical) self.assertFalse(state['eth1'].critical) def test_eq(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false eth1: dhcp4: false''') # netplan.State __getitem__ doesn't cache the netdefs, # so fetching it twice should create two separate Python objects # pointing to the same C struct. self.assertEqual(state['eth0'], state['eth0']) self.assertNotEqual(state['eth0'], state['eth1']) # Test against a weird singleton to ensure consistency against other types self.assertNotEqual(state['eth0'], True) def test_filepath(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''', filename="a.yaml") netdef = state['eth0'] self.assertEqual(os.path.join(self.confdir, "a.yaml"), netdef.filepath) @unittest.skipIf(not os.path.exists(OPENVSWITCH_OVS_VSCTL), 'OpenVSwitch not installed') def test_filepath_for_ovs_ports(self): state = state_from_yaml(self.confdir, '''network: version: 2 renderer: networkd bridges: br0: interfaces: - patch0-2 br1: interfaces: - patch2-0 openvswitch: ports: - [patch0-2, patch2-0]''', filename="a.yaml") netdef_port1 = state["patch2-0"] netdef_port2 = state["patch0-2"] self.assertEqual(os.path.join(self.confdir, "a.yaml"), netdef_port1.filepath) self.assertEqual(os.path.join(self.confdir, "a.yaml"), netdef_port2.filepath) @unittest.skipIf(not os.path.exists(OPENVSWITCH_OVS_VSCTL), 'OpenVSwitch not installed') def test_filepath_for_ovs_ports_when_conf_is_redefined(self): state = netplan.State() parser = netplan.Parser() with tempfile.NamedTemporaryFile() as f: f.write(b'''network: version: 2 renderer: networkd bridges: br0: interfaces: - patch0-2 br1: interfaces: - patch2-0 openvswitch: ports: - [patch0-2, patch2-0]''') f.flush() parser.load_yaml(f.name) with tempfile.NamedTemporaryFile() as f: f.write(b'''network: version: 2 renderer: networkd bridges: br0: interfaces: - patch0-2 br1: interfaces: - patch2-0 openvswitch: ports: - [patch0-2, patch2-0]''') f.flush() parser.load_yaml(f.name) yaml_redefinition_filepath = f.name state.import_parser_results(parser) netdef_port1 = state["patch2-0"] netdef_port2 = state["patch0-2"] self.assertEqual(os.path.join(self.confdir, yaml_redefinition_filepath), netdef_port1.filepath) self.assertEqual(os.path.join(self.confdir, yaml_redefinition_filepath), netdef_port2.filepath) def test_set_name(self): state = state_from_yaml(self.confdir, '''network: ethernets: mac-match: set-name: mymac0 match: macaddress: 11:22:33:AA:BB:FF''') self.assertEqual(state['mac-match'].set_name, 'mymac0') def test_simple_matches(self): state = state_from_yaml(self.confdir, '''network: ethernets: witness: {} name-match: match: name: "eth42" driver-match: match: driver: "e10*" mac-match: match: macaddress: 11:22:33:AA:BB:FF''') self.assertFalse(state['witness']._has_match) self.assertTrue(state['name-match']._has_match) self.assertTrue(state['name-match']._match_interface(iface_name="eth42")) self.assertFalse(state['name-match']._match_interface(iface_name="eth32")) self.assertTrue(state['driver-match']._match_interface(iface_driver="e1000")) self.assertFalse(state['name-match']._match_interface(iface_driver="ixgbe")) self.assertFalse(state['driver-match']._match_interface(iface_name="eth42")) self.assertTrue(state['mac-match']._match_interface(iface_mac="11:22:33:AA:BB:FF")) self.assertFalse(state['mac-match']._match_interface(iface_mac="11:22:33:AA:BB:CC")) def test_match_without_match_block(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''') netdef = state['eth0'] self.assertTrue(netdef._match_interface('eth0')) self.assertFalse(netdef._match_interface('eth000')) def test_vlan_props_without_vlan(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false''') self.assertIsNone(state['eth0']._vlan_id) self.assertIsNone(state['eth0'].links.get('vlan')) def test_is_trivial_compound_itf(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false bridges: br0: dhcp4: false br1: parameters: priority: 42 ''') self.assertFalse(state['eth0']._is_trivial_compound_itf) self.assertTrue(state['br0']._is_trivial_compound_itf) self.assertFalse(state['br1']._is_trivial_compound_itf) def test_interface_has_pointer_to_bridge(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false bridges: br0: dhcp4: false interfaces: - eth0 ''') self.assertEqual(state['eth0'].links.get('bridge').id, "br0") def test_interface_pointer_to_bridge_is_none(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false ''') self.assertIsNone(state['eth0'].links.get('bridge')) def test_interface_has_pointer_to_bond(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false bonds: bond0: dhcp4: false interfaces: - eth0 ''') self.assertEqual(state['eth0'].links.get('bond').id, "bond0") def test_interface_pointer_to_bond_is_none(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false ''') self.assertIsNone(state['eth0'].links.get('bond')) def test_interface_has_pointer_to_vrf(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false vrfs: vrf0: table: 1000 interfaces: - eth0 ''') self.assertEqual(state['eth0'].links.get('vrf').id, "vrf0") def test_interface_pointer_to_vrf_is_none(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false ''') self.assertIsNone(state['eth0'].links.get('vrf')) @unittest.skipIf(not os.path.exists(OPENVSWITCH_OVS_VSCTL), 'OpenVSwitch not installed') def test_interface_has_pointer_to_peer(self): state = state_from_yaml(self.confdir, '''network: openvswitch: ports: - [patch0-1, patch1-0] bonds: bond0: interfaces: - patch1-0 bridges: ovs0: interfaces: [patch0-1, bond0] ''') self.assertEqual(state['patch0-1'].links.get('peer').id, "patch1-0") self.assertEqual(state['patch1-0'].links.get('peer').id, "patch0-1") def test_dhcp4_dhcp6(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: true dhcp6: false ''') self.assertTrue(state['eth0'].dhcp4) self.assertFalse(state['eth0'].dhcp6) state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false dhcp6: true ''') self.assertFalse(state['eth0'].dhcp4) self.assertTrue(state['eth0'].dhcp6) def test_link_local(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: link-local: [ipv4, ipv6] ''') self.assertIn('ipv4', state['eth0'].link_local) self.assertIn('ipv6', state['eth0'].link_local) state = state_from_yaml(self.confdir, '''network: ethernets: eth0: link-local: [] ''') self.assertNotIn('ipv4', state['eth0'].link_local) self.assertNotIn('ipv6', state['eth0'].link_local) state = state_from_yaml(self.confdir, '''network: ethernets: eth0: link-local: [ipv4] ''') self.assertIn('ipv4', state['eth0'].link_local) self.assertNotIn('ipv6', state['eth0'].link_local) state = state_from_yaml(self.confdir, '''network: ethernets: eth0: link-local: [ipv6] ''') self.assertNotIn('ipv4', state['eth0'].link_local) self.assertIn('ipv6', state['eth0'].link_local) def test_get_macaddress(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: macaddress: aa:bb:cc:dd:ee:ff ''') self.assertEqual(state['eth0'].macaddress, 'aa:bb:cc:dd:ee:ff') state = state_from_yaml(self.confdir, '''network: ethernets: eth0: {}''') self.assertIsNone(state['eth0'].macaddress) class TestFreeFunctions(TestBase): def test_create_yaml_patch_dict(self): with tempfile.TemporaryFile() as patchfile: payload = {'ethernets': { 'eth0': {'dhcp4': True}, 'eth1': {'dhcp4': False}}} netplan._create_yaml_patch(['network'], payload, patchfile) patchfile.seek(0, io.SEEK_SET) self.assertDictEqual(payload, yaml.safe_load(patchfile.read())['network']) def test_create_yaml_patch_bad_syntax(self): with tempfile.TemporaryFile() as patchfile: with self.assertRaises(netplan.NetplanFormatException) as context: netplan._create_yaml_patch(['network'], '{invalid_yaml]', patchfile) self.assertIn('Error parsing YAML', str(context.exception)) patchfile.seek(0, io.SEEK_END) self.assertEqual(patchfile.tell(), 0) def test_dump_yaml_subtree_bad_input_file_perms(self): input_file = os.path.join(self.workdir.name, 'input.yaml') with open(input_file, "w") as f, tempfile.TemporaryFile() as output: with self.assertRaises(netplan.NetplanFileException) as context: netplan._dump_yaml_subtree(['network'], f, output) self.assertIn('Invalid argument', str(context.exception)) def test_dump_yaml_subtree_bad_output_file_perms(self): input_file = os.path.join(self.workdir.name, 'input.yaml') output_file = os.path.join(self.workdir.name, 'output.yaml') with open(input_file, 'w') as input, open(output_file, 'w') as output: input.write('network: {}') output.write('') with open(input_file, "r") as f, open(output_file, 'r') as output: with self.assertRaises(netplan.NetplanFileException) as context: netplan._dump_yaml_subtree(['network'], f, output) self.assertIn('Invalid argument', str(context.exception)) def test_dump_yaml_subtree_bad_yaml_outside(self): input_file = os.path.join(self.workdir.name, 'input.yaml') with open(input_file, "w+") as f, tempfile.TemporaryFile() as output: f.write('{garbage)') f.flush() with self.assertRaises(netplan.NetplanFormatException) as context: netplan._dump_yaml_subtree(['network'], f, output) self.assertIn('Error parsing YAML', str(context.exception)) def test_dump_yaml_subtree_bad_yaml_inside(self): input_file = os.path.join(self.workdir.name, 'input.yaml') with open(input_file, "w+") as f, tempfile.TemporaryFile() as output: f.write('''network: ethernets: {garbage)''') f.flush() with self.assertRaises(netplan.NetplanFormatException) as context: netplan._dump_yaml_subtree(['network'], f, output) self.assertIn('Error parsing YAML', str(context.exception)) def test_dump_yaml_subtree_bad_type(self): input_file = os.path.join(self.workdir.name, 'input.yaml') with open(input_file, "w+") as f, tempfile.TemporaryFile() as output: f.write('''[]''') f.flush() with self.assertRaises(netplan.NetplanFormatException) as context: netplan._dump_yaml_subtree(['network'], f, output) self.assertIn('Unexpected YAML structure found', str(context.exception)) def test_dump_yaml_subtree_bad_yaml_ignored(self): input_file = os.path.join(self.workdir.name, 'input.yaml') with open(input_file, "w+") as f, tempfile.TemporaryFile() as output: f.write('''network: ethernets: null ignored: - [}''') f.flush() with self.assertRaises(netplan.NetplanFormatException) as context: netplan._dump_yaml_subtree(['network'], f, output) self.assertIn('Error parsing YAML', str(context.exception)) def test_dump_yaml_subtree_discard_tail(self): input_file = os.path.join(self.workdir.name, 'input.yaml') with open(input_file, "w+") as f, tempfile.TemporaryFile() as output: f.write('''network: ethernets: {} tail: - []''') f.flush() netplan._dump_yaml_subtree(['network', 'ethernets'], f, output) output.seek(0) self.assertEqual(yaml.safe_load(output), {}) def test_dump_yaml_absent_key(self): input_file = os.path.join(self.workdir.name, 'input.yaml') with open(input_file, "w+") as f, tempfile.TemporaryFile() as output: f.write('''network: ethernets: {} tail: - []''') f.flush() netplan._dump_yaml_subtree(['network', 'ethernets', 'eth0'], f, output) output.seek(0) self.assertEqual(yaml.safe_load(output), None) def test_validation_error_exception(self): ''' "set-name" requires "match" so it should fail validation ''' parser = netplan.Parser() with tempfile.TemporaryDirectory() as d: full_dir = d + '/etc/netplan' os.makedirs(full_dir) with tempfile.NamedTemporaryFile(suffix='.yaml', dir=full_dir) as f: f.write(b'''network: ethernets: eth0: set-name: abc''') f.flush() with self.assertRaises(netplan.NetplanValidationException): parser.load_yaml_hierarchy(d) def test_validation_exception_with_bad_error_message(self): ''' If the exception's constructor can't parse the error message it will raise a ValueError exception. This situation should never happen though. ''' with self.assertRaises(ValueError): netplan.NetplanValidationException('not the expected file path', 0, 0) def test_parser_exception_with_bad_error_message(self): ''' If the exception's constructor can't parse the error message it will raise a ValueError exception. This situation should never happen though. ''' with self.assertRaises(ValueError): netplan.NetplanParserException('not the expected file path, line and column', 0, 0) def test_netdef_get_bond_mode(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false bonds: bond0: parameters: mode: active-backup dhcp4: false interfaces: - eth0 ''') self.assertEqual(state['bond0']._bond_mode, "active-backup") def test_netdef_get_bond_mode_unset(self): state = state_from_yaml(self.confdir, '''network: ethernets: eth0: dhcp4: false bonds: bond0: dhcp4: false interfaces: - eth0 ''') self.assertIsNone(state['bond0']._bond_mode) netplan-1.0/tests/test_ovs.py000066400000000000000000000144141457004145200164030ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2020 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import unittest from unittest.mock import patch, call from netplan_cli.cli.ovs import OPENVSWITCH_OVS_VSCTL as OVS import netplan_cli.cli.ovs as ovs from utils import state_from_yaml import tempfile @unittest.skipIf(not os.path.exists(OVS), 'OpenVSwitch not installed') class TestOVS(unittest.TestCase): @patch('subprocess.check_call') def test_clear_settings_tag(self, mock): ovs.clear_setting('Bridge', 'ovs0', 'netplan/external-ids/key', 'value') mock.assert_called_with([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/external-ids/key']) @patch('subprocess.check_output') @patch('subprocess.check_call') def test_clear_global_ssl(self, mock, mock_out): mock_out.return_value = ''' Private key: /private/key.pem Certificate: /another/cert.pem CA Certificate: /some/ca-cert.pem Bootstrap: false''' ovs.clear_setting('Open_vSwitch', '.', 'netplan/global/set-ssl', '/private/key.pem,/another/cert.pem,/some/ca-cert.pem') mock_out.assert_called_once_with([OVS, 'get-ssl'], text=True) mock.assert_has_calls([ call([OVS, 'del-ssl']), call([OVS, 'remove', 'Open_vSwitch', '.', 'external-ids', 'netplan/global/set-ssl']) ]) @patch('subprocess.check_output') @patch('subprocess.check_call') def test_no_clear_global_ssl_different(self, mock, mock_out): mock_out.return_value = ''' Private key: /private/key.pem Certificate: /another/cert.pem CA Certificate: /some/ca-cert.pem Bootstrap: false''' ovs.clear_setting('Open_vSwitch', '.', 'netplan/global/set-ssl', '/some/key.pem,/other/cert.pem,/some/cert.pem') mock_out.assert_called_once_with([OVS, 'get-ssl'], text=True) mock.assert_has_calls([ call([OVS, 'remove', 'Open_vSwitch', '.', 'external-ids', 'netplan/global/set-ssl']) ]) def test_clear_global_unknown(self): with self.assertRaises(Exception): ovs.clear_setting('Bridge', 'ovs0', 'netplan/global/set-something', 'INVALID') @patch('subprocess.check_output') @patch('subprocess.check_call') def test_clear_global(self, mock, mock_out): mock_out.return_value = 'tcp:127.0.0.1:1337\nunix:/some/socket' ovs.clear_setting('Bridge', 'ovs0', 'netplan/global/set-controller', 'tcp:127.0.0.1:1337,unix:/some/socket') mock_out.assert_called_once_with([OVS, 'get-controller', 'ovs0'], text=True) mock.assert_has_calls([ call([OVS, 'del-controller', 'ovs0']), call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/global/set-controller']) ]) @patch('subprocess.check_output') @patch('subprocess.check_call') def test_no_clear_global_different(self, mock, mock_out): mock_out.return_value = 'unix:/var/run/openvswitch/ovs0.mgmt' ovs.clear_setting('Bridge', 'ovs0', 'netplan/global/set-controller', 'tcp:127.0.0.1:1337,unix:/some/socket') mock_out.assert_called_once_with([OVS, 'get-controller', 'ovs0'], text=True) mock.assert_has_calls([ call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/global/set-controller']) ]) @patch('subprocess.check_call') def test_clear_dict(self, mock): ovs.clear_setting('Bridge', 'ovs0', 'netplan/other-config/key', 'value') mock.assert_has_calls([ call([OVS, 'remove', 'Bridge', 'ovs0', 'other-config', 'key', 'value']), call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/other-config/key']) ]) @patch('subprocess.check_call') def test_clear_col(self, mock): ovs.clear_setting('Port', 'bond0', 'netplan/bond_mode', 'balance-tcp') mock.assert_has_calls([ call([OVS, 'remove', 'Port', 'bond0', 'bond_mode', 'balance-tcp']), call([OVS, 'remove', 'Port', 'bond0', 'external-ids', 'netplan/bond_mode']) ]) @patch('subprocess.check_call') def test_clear_col_default(self, mock): ovs.clear_setting('Bridge', 'ovs0', 'netplan/rstp_enable', 'true') mock.assert_has_calls([ call([OVS, 'set', 'Bridge', 'ovs0', 'rstp_enable=false']), call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/rstp_enable']) ]) @patch('subprocess.check_call') def test_clear_dict_colon(self, mock): ovs.clear_setting('Bridge', 'ovs0', 'netplan/other-config/key', 'fa:16:3e:4b:19:3a') mock.assert_has_calls([ call([OVS, 'remove', 'Bridge', 'ovs0', 'other-config', 'key', r'fa\:16\:3e\:4b\:19\:3a']), call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/other-config/key']) ]) mock.mock_calls def test_is_ovs_interface(self): with tempfile.TemporaryDirectory() as root: state = state_from_yaml(root, '''network: ethernets: ovs0: openvswitch: {}''') self.assertTrue(ovs.is_ovs_interface('ovs0', state.netdefs)) def test_is_ovs_interface_false(self): with tempfile.TemporaryDirectory() as root: state = state_from_yaml(root, '''network: ethernets: eth0: {} eth1: {} bridges: br0: interfaces: - eth0 - eth1''') self.assertFalse(ovs.is_ovs_interface('br0', state.netdefs)) def test_is_ovs_interface_recursive(self): with tempfile.TemporaryDirectory() as root: state = state_from_yaml(root, '''network: version: 2 openvswitch: ports: - [patch0-1, patch1-0] ethernets: eth0: {} bonds: bond0: interfaces: [patch1-0, eth0]''') self.assertTrue(ovs.is_ovs_interface('bond0', state.netdefs)) netplan-1.0/tests/test_sriov.py000066400000000000000000001367171457004145200167510ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2020-2022 Canonical, Ltd. # Author: Łukasz 'sil2100' Zemczak # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from io import StringIO import os import subprocess import tempfile import unittest from subprocess import CalledProcessError from collections import defaultdict from unittest.mock import patch, mock_open, call from netplan_cli.cli.commands.sriov_rebind import INTERVAL_SEC, MAX_WAITING_TIME_SEC, NetplanSriovRebind import netplan_cli.cli.sriov as sriov from netplan_cli.configmanager import ConfigManager, ConfigurationError from generator.base import TestBase from tests.test_utils import call_cli class MockSRIOVOpen(): def __init__(self): # now this is a VERY ugly hack to make mock_open() better self.read_queue = [] self.write_queue = [] def sriov_read(): action = self.read_queue.pop(0) if isinstance(action, str): return action else: raise action def sriov_write(data): if not self.write_queue: return action = self.write_queue.pop(0) if isinstance(action, Exception): raise action self.open = mock_open() self.open.return_value.read.side_effect = sriov_read self.open.return_value.write.side_effect = sriov_write def mock_set_counts(interfaces, config_manager, vf_counts, active_vfs, active_pfs): counts = {'enp1': 2, 'enp2': 1} vfs = {'enp1s16f1': None, 'enp1s16f2': None, 'customvf1': None} pfs = {'enp1': 'enp1', 'enpx': 'enp2'} vf_counts.update(counts) active_vfs.update(vfs) active_pfs.update(pfs) class TestSRIOV(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() os.makedirs(os.path.join(self.workdir.name, 'etc/netplan')) self.configmanager = ConfigManager(prefix=self.workdir.name, extra_files={}) def _prepare_sysfs_dir_structure(self, pf=('enp2', '0000:00:1f.0'), vfs=[('enp2s16f1', '0000:00:1f.6')], pf_driver='fake_driver'): """ Setup sysfs mock for reading certain SR-IOV related files and symlinks :param tuple pf: A tuple descibing the physical function (iterface_name, pci_address) :param list vfs: A list of tuples describing the virtual functions related to this PF :param str pf_driver: The driver name to be mocked for this PF """ pf_iface, pf_pci_addr = pf # prepare a directory hierarchy for testing the matching # this might look really scary, but that's how sysfs presents devices # such as these sysfs = os.path.join(self.workdir.name, 'sys') sys_devices = os.path.join(sysfs, 'devices/pci0000:00') pci_devices = os.path.join(sysfs, 'bus/pci/devices') pci_driver = os.path.join(sysfs, 'bus/pci/drivers', pf_driver) os.makedirs(os.path.join(sysfs, 'class/net'), exist_ok=True) os.makedirs(pci_devices, exist_ok=True) os.makedirs(pci_driver, exist_ok=True) # access to 'bind' and 'unbind' files must be mocked # create the PF (enp2) dir # syfs mock in: # sys/devices/pci0000:00/PCI_ADDR # sys/devices/pci0000:00/PCI_ADDR/net/IFACE pf_iface_path = os.path.join(sys_devices, pf_pci_addr, 'net', pf_iface) pf_dev_path = os.path.join(sys_devices, pf_pci_addr) os.makedirs(pf_iface_path) # symlink it to /sys/bus/pci/devices os.symlink(os.path.join('../../../devices/pci0000:00', pf_pci_addr), os.path.join(pci_devices, pf_pci_addr)) # create VF (enp2s16f1, ...) dirs # sysfs mock in: # sys/devices/pci0000:00/PCI_ADDR and # sys/devices/pci0000:00/PCI_ADDR/net/IFACE for vf_iface, vf_pci_addr in vfs: vf_iface_path = os.path.join(sys_devices, vf_pci_addr, 'net', vf_iface) vf_dev_path = os.path.join(sys_devices, vf_pci_addr) os.makedirs(vf_iface_path) # symlink it to /sys/bus/pci/devices os.symlink(os.path.join('../../../devices/pci0000:00', vf_pci_addr), os.path.join(pci_devices, vf_pci_addr)) # populate the VF data with open(os.path.join(vf_dev_path, 'vendor'), 'w') as f: f.write('0x001f\n') with open(os.path.join(vf_dev_path, 'device'), 'w') as f: f.write('0xb33f\n') os.symlink(os.path.join('../../devices/pci0000:00', vf_pci_addr, 'net', vf_iface), os.path.join(sysfs, 'class/net', vf_iface)) os.symlink(os.path.join('../../..', vf_pci_addr), os.path.join(sysfs, 'class/net', vf_iface, 'device')) # the VFs additionally have a device link to the PF os.symlink(os.path.join('../../..', pf_pci_addr), os.path.join(vf_dev_path, 'physfn')) # populate the PF data with open(os.path.join(pf_dev_path, 'vendor'), 'w') as f: f.write('0x001f\n') with open(os.path.join(pf_dev_path, 'device'), 'w') as f: f.write('0x1337\n') with open(os.path.join(pf_dev_path, 'sriov_numvfs'), 'w') as f: f.write(str(len(vfs))+'\n') os.symlink(os.path.join('../../../bus/pci/drivers', pf_driver), os.path.join(pf_dev_path, 'driver')) os.symlink(os.path.join('../../devices/pci0000:00', pf_pci_addr, 'net', pf_iface), os.path.join(sysfs, 'class/net', pf_iface)) os.symlink(os.path.join('../../..', pf_pci_addr), os.path.join(sysfs, 'class/net', pf_iface, 'device')) # the PF additionally has device links to all the VFs defined for it for i in range(len(vfs)): os.symlink(os.path.join('../../..', vfs[i][1]), os.path.join(pf_dev_path, 'virtfn'+str(i))) @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_get_vf_count_and_functions(self, gim, gidn): # we mock-out get_interface_driver_name and get_interface_macaddress # to return useful values for the test gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'enp3' else '00:00:00:00:00:00' gidn.side_effect = lambda x: 'foo' if x == 'enp2' else 'bar' with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: renderer: networkd enp1: mtu: 9000 enp2: match: driver: foo enp3: match: macaddress: 00:01:02:03:04:05 enpx: match: name: enp[4-5] enp0: mtu: 9000 enp8: virtual-function-count: 7 enp9: {} wlp6s0: {} enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 macaddress: 01:02:03:04:05:01 enp2s16f1: link: enp2 enp2s16f2: {link: enp2} enp3s16f1: link: enp3 enpxs16f1: match: name: enp[4-5]s16f1 link: enpx enp9s16f1: link: enp9 ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'enp2', 'enp3', 'enp5', 'enp0', 'enp8'] vf_counts = defaultdict(int) vfs = {} pfs = {} # call the function under test sriov.get_vf_count_and_functions(interfaces, self.configmanager.np_state, vf_counts, vfs, pfs) # check if the right vf counts have been recorded in vf_counts self.assertDictEqual( vf_counts, {'enp1': 2, 'enp2': 2, 'enp3': 1, 'enp5': 1, 'enp8': 7}) # also check if the vfs and pfs dictionaries got properly set self.assertDictEqual( vfs, {'enp1s16f1': None, 'enp1s16f2': None, 'enp2s16f1': None, 'enp2s16f2': None, 'enp3s16f1': None, 'enpxs16f1': None}) self.assertDictEqual( pfs, {'enp1': 'enp1', 'enp2': 'enp2', 'enp3': 'enp3', 'enpx': 'enp5', 'enp8': 'enp8'}) @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_get_vf_count_and_functions_set_name(self, gim, gidn): # we mock-out get_interface_driver_name and get_interface_macaddress # to return useful values for the test gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'enp3' else '00:00:00:00:00:00' gidn.side_effect = lambda x: 'foo' if x == 'enp1' else 'bar' with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: renderer: networkd enp1: match: driver: foo set-name: pf1 enp8: match: name: enp[3-8] set-name: pf2 virtual-function-count: 7 enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 ''', file=fd) self.configmanager.parse() interfaces = ['pf1', 'enp8'] vf_counts = defaultdict(int) vfs = {} pfs = {} # call the function under test sriov.get_vf_count_and_functions(interfaces, self.configmanager.np_state, vf_counts, vfs, pfs) # check if the right vf counts have been recorded in vf_counts - # we expect netplan to take into consideration the renamed interface # names here self.assertDictEqual( vf_counts, {'pf1': 1, 'enp8': 7}) # also check if the vfs and pfs dictionaries got properly set self.assertDictEqual( vfs, {'enp1s16f1': None}) self.assertDictEqual( pfs, {'enp1': 'pf1', 'enp8': 'enp8'}) @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_get_vf_count_and_functions_many_match(self, gim, gidn): # we mock-out get_interface_driver_name and get_interface_macaddress # to return useful values for the test gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'enp3' else '00:00:00:00:00:00' gidn.side_effect = lambda x: 'foo' if x == 'enp2' else 'bar' with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: renderer: networkd enpx: match: name: enp* mtu: 9000 enpxs16f1: link: enpx ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'wlp6s0', 'enp2', 'enp3'] vf_counts = defaultdict(int) vfs = {} pfs = {} # call the function under test with self.assertRaises(ConfigurationError) as e: sriov.get_vf_count_and_functions(interfaces, self.configmanager.np_state, vf_counts, vfs, pfs) self.assertIn('matched more than one interface for a PF device: enpx', str(e.exception)) @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_get_vf_count_and_functions_not_enough_explicit(self, gim, gidn): # we mock-out get_interface_driver_name and get_interface_macaddress # to return useful values for the test gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'enp3' else '00:00:00:00:00:00' gidn.side_effect = lambda x: 'foo' if x == 'enp2' else 'bar' with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: renderer: networkd enp1: virtual-function-count: 2 mtu: 9000 enp1s16f1: link: enp1 enp1s16f2: link: enp1 enp1s16f3: link: enp1 ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'wlp6s0'] vf_counts = defaultdict(int) vfs = {} pfs = {} # call the function under test with self.assertRaises(ConfigurationError) as e: sriov.get_vf_count_and_functions(interfaces, self.configmanager.np_state, vf_counts, vfs, pfs) self.assertIn('more VFs allocated than the explicit size declared: 3 > 2', str(e.exception)) def test_set_numvfs_for_pf(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['8\n'] with patch('builtins.open', sriov_open.open): ret = sriov.set_numvfs_for_pf('enp1', 2) self.assertTrue(ret) self.assertListEqual(sriov_open.open.call_args_list, [call('/sys/class/net/enp1/device/sriov_totalvfs'), call('/sys/class/net/enp1/device/sriov_numvfs', 'w')]) handle = sriov_open.open() handle.write.assert_called_once_with('2') def test_set_numvfs_for_pf_failsafe(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['8\n'] sriov_open.write_queue = [IOError(16, 'Error'), None, None] with patch('builtins.open', sriov_open.open): ret = sriov.set_numvfs_for_pf('enp1', 2) self.assertTrue(ret) handle = sriov_open.open() self.assertEqual(handle.write.call_count, 3) def test_set_numvfs_for_pf_over_max(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['8\n'] with patch('builtins.open', sriov_open.open): with self.assertRaises(ConfigurationError) as e: sriov.set_numvfs_for_pf('enp1', 9) self.assertIn('cannot allocate more VFs for PF enp1 than supported', str(e.exception)) def test_set_numvfs_for_pf_over_theoretical_max(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['1337\n'] with patch('builtins.open', sriov_open.open): with self.assertRaises(ConfigurationError) as e: sriov.set_numvfs_for_pf('enp1', 345) self.assertIn('cannot allocate more VFs for PF enp1 than the SR-IOV maximum', str(e.exception)) def test_set_numvfs_for_pf_read_failed(self): sriov_open = MockSRIOVOpen() cases = ( [IOError], ['not a number\n'], ) with patch('builtins.open', sriov_open.open): for case in cases: sriov_open.read_queue = case with self.assertRaises(RuntimeError): sriov.set_numvfs_for_pf('enp1', 3) def test_set_numvfs_for_pf_write_failed(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['8\n'] sriov_open.write_queue = [IOError(16, 'Error'), IOError(16, 'Error')] with patch('builtins.open', sriov_open.open): with self.assertRaises(RuntimeError) as e: sriov.set_numvfs_for_pf('enp1', 2) self.assertIn('failed setting sriov_numvfs to 2 for enp1', str(e.exception)) def test_perform_hardware_specific_quirks(self): # for now we have no custom quirks defined, so we just # check if the function succeeds sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['0x001f\n', '0x1337\n'] with patch('builtins.open', sriov_open.open): sriov.perform_hardware_specific_quirks('enp1') # it's good enough if it did all the matching self.assertListEqual(sriov_open.open.call_args_list, [call('/sys/class/net/enp1/device/vendor'), call('/sys/class/net/enp1/device/device'), ]) def test_perform_hardware_specific_quirks_failed(self): sriov_open = MockSRIOVOpen() cases = ( [IOError], ['0x001f\n', IOError], ) with patch('builtins.open', sriov_open.open): for case in cases: sriov_open.read_queue = case with self.assertRaises(RuntimeError) as e: sriov.perform_hardware_specific_quirks('enp1') self.assertIn('could not determine vendor and device ID of enp1', str(e.exception)) @patch('subprocess.check_call') def test_apply_vlan_filter_for_vf(self, check_call): self._prepare_sysfs_dir_structure() sriov.apply_vlan_filter_for_vf('enp2', 'enp2s16f1', 'vlan10', 10, prefix=self.workdir.name) self.assertEqual(check_call.call_count, 1) self.assertListEqual(check_call.call_args[0][0], ['ip', 'link', 'set', 'dev', 'enp2', 'vf', '0', 'vlan', '10']) @patch('subprocess.check_call') def test_apply_vlan_filter_for_vf_failed_no_index(self, check_call): self._prepare_sysfs_dir_structure(vfs=[('enp2s14f1', '0000:00:1f.4'), ('enp2s15f1', '0000:00:1f.5'), ('enp2s16f1', '0000:00:1f.6'), ('enp2s17f1', '0000:00:1f.7')]) # we remove the PF -> VF link, simulating a system error os.unlink(os.path.join(self.workdir.name, 'sys/class/net/enp2/device/virtfn2')) with self.assertRaises(RuntimeError) as e: sriov.apply_vlan_filter_for_vf('enp2', 'enp2s16f1', 'vlan10', 10, prefix=self.workdir.name) self.assertIn('could not determine the VF index for enp2s16f1 while configuring vlan vlan10', str(e.exception)) self.assertEqual(check_call.call_count, 0) @patch('subprocess.check_call') def test_apply_vlan_filter_for_vf_failed_ip_link_set(self, check_call): self._prepare_sysfs_dir_structure() check_call.side_effect = CalledProcessError(-1, None) with self.assertRaises(RuntimeError) as e: sriov.apply_vlan_filter_for_vf('enp2', 'enp2s16f1', 'vlan10', 10, prefix=self.workdir.name) self.assertIn('failed setting SR-IOV VLAN filter for vlan vlan10', str(e.exception)) @patch('netifaces.interfaces') @patch('netplan_cli.cli.sriov.get_vf_count_and_functions') @patch('netplan_cli.cli.sriov.set_numvfs_for_pf') @patch('netplan_cli.cli.sriov.perform_hardware_specific_quirks') @patch('netplan_cli.cli.sriov.apply_vlan_filter_for_vf') @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_apply_sriov_config(self, gim, gidn, apply_vlan, quirks, set_numvfs, get_counts, netifs): # set up the environment with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: mtu: 9000 enpx: match: name: enp[2-3] enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 customvf1: match: name: enp[2-3]s16f[1-4] link: enpx vlans: vf1.15: renderer: sriov id: 15 link: customvf1 ''', file=fd) # set up all the mock objects netifs.return_value = ['enp1', 'enp2', 'enp5', 'wlp6s0', 'enp1s16f1', 'enp1s16f2', 'enp2s16f1'] get_counts.side_effect = mock_set_counts set_numvfs.side_effect = lambda pf, _: False if pf == 'enp2' else True gidn.return_value = 'foodriver' gim.return_value = '00:01:02:03:04:05' # call method under test sriov.apply_sriov_config(self.configmanager, rootdir=self.workdir.name) # make sure config_manager.parse() has been called self.assertTrue(self.configmanager.np_state) # check if the config got applied as expected # we had 2 PFs, one having two VFs and the other only one self.assertEqual(set_numvfs.call_count, 2) self.assertListEqual(set_numvfs.call_args_list, [call('enp1', 2), call('enp2', 1)]) # one of the pfs already had sufficient VFs allocated, so only enp1 # changed the vf count and only that one should trigger quirks quirks.assert_called_once_with('enp1') # only one had a hardware vlan apply_vlan.assert_called_once_with('enp2', 'enp2s16f1', 'vf1.15', 15) @patch('netifaces.interfaces') @patch('netplan_cli.cli.sriov.get_vf_count_and_functions') @patch('netplan_cli.cli.sriov.set_numvfs_for_pf') @patch('netplan_cli.cli.sriov.perform_hardware_specific_quirks') @patch('netplan_cli.cli.sriov.apply_vlan_filter_for_vf') @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_apply_sriov_config_invalid_vlan(self, gim, gidn, apply_vlan, quirks, set_numvfs, get_counts, netifs): # set up the environment with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: mtu: 9000 enpx: match: name: enp[2-3] enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 customvf1: match: name: enp[2-3]s16f[1-4] link: enpx vlans: vf1.15: renderer: sriov link: customvf1 ''', file=fd) # set up all the mock objects netifs.return_value = ['enp1', 'enp2', 'enp5', 'wlp6s0', 'enp1s16f1', 'enp1s16f2', 'enp2s16f1'] get_counts.side_effect = mock_set_counts set_numvfs.side_effect = lambda pf, _: False if pf == 'enp2' else True gidn.return_value = 'foodriver' gim.return_value = '00:01:02:03:04:05' # call method under test with self.assertRaises(ConfigurationError) as e: sriov.apply_sriov_config(self.configmanager, rootdir=self.workdir.name) self.assertIn('vf1.15: missing \'id\' property', str(e.exception)) self.assertEqual(apply_vlan.call_count, 0) def test_apply_sriov_invalid_link_no_vf(self): # set up the environment with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: ethernets: enp1: {} vlans: vf1.15: renderer: sriov id: 15 link: enp1 ''', file=fd) # call method under test with self.assertLogs() as logs: sriov.apply_sriov_config(self.configmanager, rootdir=self.workdir.name) self.assertIn('SR-IOV vlan defined for vf1.15 but link enp1 is ' 'either not a VF or has no matches', logs.output[0]) @patch('netifaces.interfaces') @patch('netplan_cli.cli.sriov.get_vf_count_and_functions') @patch('netplan_cli.cli.sriov.set_numvfs_for_pf') @patch('netplan_cli.cli.sriov.perform_hardware_specific_quirks') @patch('netplan_cli.cli.sriov.apply_vlan_filter_for_vf') @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_apply_sriov_config_too_many_vlans(self, gim, gidn, apply_vlan, quirks, set_numvfs, get_counts, netifs): # set up the environment with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: mtu: 9000 enpx: match: name: enp[2-3] enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 customvf1: match: name: enp[2-3]s16f[1-4] link: enpx vlans: vf1.15: renderer: sriov id: 15 link: customvf1 vf1.16: renderer: sriov id: 16 link: customvf1 ''', file=fd) # set up all the mock objects netifs.return_value = ['enp1', 'enp2', 'enp5', 'wlp6s0', 'enp1s16f1', 'enp1s16f2', 'enp2s16f1'] get_counts.side_effect = mock_set_counts set_numvfs.side_effect = lambda pf, _: False if pf == 'enp2' else True gidn.return_value = 'foodriver' gim.return_value = '00:01:02:03:04:05' # call method under test with self.assertRaises(ConfigurationError) as e: sriov.apply_sriov_config(self.configmanager, rootdir=self.workdir.name) self.assertIn('interface enp2s16f1 for netplan device customvf1 (vf1.16) already has an SR-IOV vlan defined', str(e.exception)) self.assertEqual(apply_vlan.call_count, 1) @patch('netifaces.interfaces') @patch('netplan_cli.cli.sriov.get_vf_count_and_functions') @patch('netplan_cli.cli.sriov.set_numvfs_for_pf') @patch('netplan_cli.cli.sriov.perform_hardware_specific_quirks') @patch('netplan_cli.cli.sriov.apply_vlan_filter_for_vf') @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_apply_sriov_config_many_match(self, gim, gidn, apply_vlan, quirks, set_numvfs, get_counts, netifs): # set up the environment with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: mtu: 9000 enpx: match: name: enp[2-3] enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 customvf1: match: name: enp*s16f[1-4] link: enpx ''', file=fd) # set up all the mock objects netifs.return_value = ['enp1', 'enp2', 'enp5', 'wlp6s0', 'enp1s16f1', 'enp1s16f2', 'enp2s16f1'] get_counts.side_effect = mock_set_counts set_numvfs.side_effect = lambda pf, _: False if pf == 'enp2' else True gidn.return_value = 'foodriver' gim.return_value = '00:01:02:03:04:05' # call method under test with self.assertRaises(ConfigurationError) as e: sriov.apply_sriov_config(self.configmanager, rootdir=self.workdir.name) self.assertIn('matched more than one interface for a VF device: customvf1', str(e.exception)) def test_unit_get_pci_slot_name(self): # test error case with self.assertRaises(RuntimeError) as e: sriov._get_pci_slot_name('notAnetdev0') self.assertIn('failed parsing PCI slot name for notAnetdev0:', str(e.exception)) # test success case with patch('builtins.open', mock_open(read_data='''DRIVER=e1000e PCI_CLASS=20000 PCI_ID=8086:156F PCI_SUBSYS_ID=17AA:2245 PCI_SLOT_NAME=0000:00:1f.6 MODALIAS=pci:v00008086d0000156Fsv000017AAsd00002245bc02sc00i00 ''')) as mock_file: self.assertEqual(sriov._get_pci_slot_name('eth99'), '0000:00:1f.6') mock_file.assert_called_with('/sys/class/net/eth99/device/uevent') def test_unit_class_PCIDevice(self): pcidev = sriov.PCIDevice('0000:00:1f.6') self.assertEqual('/sys', pcidev.sys) self.assertLessEqual('/sys/bus/pci/devices/0000:00:1f.6', pcidev.path) with patch('netplan_cli.cli.sriov.PCIDevice.sys', new_callable=unittest.mock.PropertyMock) as sys_mock: sys_mock.return_value = os.path.join(self.workdir.name, 'sys_mock') os.makedirs(os.path.join(self.workdir.name, 'sys_mock/bus/pci/devices/0000:00:1f.6/driver')) self.assertTrue(pcidev.bound) open(os.path.join(self.workdir.name, 'sys_mock/bus/pci/devices/0000:00:1f.6/physfn'), 'a').close() self.assertTrue(pcidev.is_vf) @patch('netifaces.interfaces') @patch('netplan_cli.cli.sriov.get_vf_count_and_functions') @patch('netplan_cli.cli.sriov.set_numvfs_for_pf') @patch('netplan_cli.cli.sriov.perform_hardware_specific_quirks') @patch('subprocess.check_call') @patch('netplan_cli.cli.sriov.PCIDevice.bound', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.sriov.PCIDevice.sys', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.sriov.PCIDevice.devlink_eswitch_mode') @patch('netplan_cli.cli.sriov._get_pci_slot_name') def test_apply_sriov_config_eswitch_mode(self, gpsn, pcidevice_devlink, pcidevice_sys, pcidevice_bound, scc, quirks, set_numvfs, get_counts, netifs): handle = mock_open() builtin_open = open # save the unpatched version of open() def driver_mock_open(*args, **kwargs): # mock only writes/opens to the mlx5_core driver's un-/bind files if args[0].endswith('mlx5_core/bind') or args[0].endswith('mlx5_core/unbind'): return handle(*args, **kwargs) # unpatched version for every other path return builtin_open(*args, **kwargs) # pragma: nocover # set up the mock sysfs environment self._prepare_sysfs_dir_structure(pf=('enp1', '0000:03:00.0'), vfs=[('enp1s16f1', '0000:03:00.2'), ('enp1s16f2', '0000:03:00.3')], pf_driver='mlx5_core') self._prepare_sysfs_dir_structure(pf=('enp2', '0000:03:00.1'), vfs=[('enp2s14f1', '0000:03:08.2'), ('enp2s15f1', '0000:03:08.3'), ('enp2s16f1', '0000:03:08.4'), ('enp2s17f1', '0000:03:08.5')], pf_driver='mlx5_core') enp1_pci_addr = '0000:03:00.0' enp2_pci_addr = '0000:03:00.1' gpsn.side_effect = lambda iface: enp1_pci_addr if iface == 'enp1' else enp2_pci_addr sys_path = os.path.join(self.workdir.name, 'sys') pcidevice_devlink.return_value = '__undetermined' pcidevice_sys.return_value = sys_path pcidevice_bound.side_effect = [ True, True, # 2x unbind (enp1 VFs) True, True, True, True, # 4x unbind (enpx/enp2 VFs) False, False, False, False] # 4x re-bind (enpx/enp2 VFs) # YAML config with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: embedded-switch-mode: "legacy" delay-virtual-functions-rebind: true enpx: match: name: enp[2-3] embedded-switch-mode: "switchdev" enp1s16f1: link: enp1 enp1s16f2: link: enp1 customvf1: match: name: enp[2-3]s16f[1-4] link: enpx ''', file=fd) # set up all the mock objects netifs.return_value = ['enp1', 'enp2', 'enp5', 'wlp6s0', 'enp1s16f1', 'enp1s16f2', 'enp2s16f1'] get_counts.side_effect = mock_set_counts writes = [ ('/sys/bus/pci/drivers/mlx5_core/unbind', '0000:03:00.2'), ('/sys/bus/pci/drivers/mlx5_core/unbind', '0000:03:00.3'), ('/sys/bus/pci/drivers/mlx5_core/unbind', '0000:03:08.2'), ('/sys/bus/pci/drivers/mlx5_core/unbind', '0000:03:08.3'), ('/sys/bus/pci/drivers/mlx5_core/unbind', '0000:03:08.4'), ('/sys/bus/pci/drivers/mlx5_core/unbind', '0000:03:08.5'), ('/sys/bus/pci/drivers/mlx5_core/bind', '0000:03:08.2'), ('/sys/bus/pci/drivers/mlx5_core/bind', '0000:03:08.3'), ('/sys/bus/pci/drivers/mlx5_core/bind', '0000:03:08.4'), ('/sys/bus/pci/drivers/mlx5_core/bind', '0000:03:08.5')] # test success case with patch('builtins.open', driver_mock_open): sriov.apply_sriov_config(self.configmanager, rootdir=self.workdir.name) self.assertEqual(len(writes), handle.call_count) self.assertEqual(handle.call_args_list, [call(elem[0], 'wt') for elem in writes]) self.assertEqual(len(writes), handle().write.call_count) self.assertEqual(handle().write.call_args_list, [call(elem[1]) for elem in writes]) self.assertEqual(2, scc.call_count) scc.assert_has_calls([ call(['/sbin/devlink', 'dev', 'eswitch', 'set', 'pci/0000:03:00.0', 'mode', 'legacy']), call(['/sbin/devlink', 'dev', 'eswitch', 'set', 'pci/0000:03:00.1', 'mode', 'switchdev']) ]) @patch('netplan_cli.cli.sriov.PCIDevice.bound', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.sriov.PCIDevice.sys', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.commands.sriov_rebind._get_pci_slot_name') def test_cli_rebind(self, gpsn, sys_mock, bound_mock): self._prepare_sysfs_dir_structure(pf=('enp3s0f0', '0000:03:00.0'), vfs=[('enp3s0f0v0', '0000:03:00.2'), ('enp3s0f0v1', '0000:03:00.3')], pf_driver='some_driver') sys_path = os.path.join(self.workdir.name, 'sys') sys_mock.return_value = sys_path enp3s0f0_pci_addr = '0000:03:00.0' not_a_pf_pci_addr = '0000:00:99.9' gpsn.side_effect = lambda iface: enp3s0f0_pci_addr if iface == 'enp3s0f0' else not_a_pf_pci_addr bound_mock.side_effect = [False, False] # 0000:03:00.2 and 0000:03:00.3 are unbound with patch('builtins.open', mock_open(read_data='')) as mock_file: out = call_cli(['rebind', 'enp3s0f0', 'not_a_pf']) self.assertEqual(out, '', msg='netplan rebind returned unexpected output') self.assertEqual(2, mock_file.call_count) self.assertEqual(mock_file.call_args_list, [ call('/sys/bus/pci/drivers/some_driver/bind', 'wt'), call('/sys/bus/pci/drivers/some_driver/bind', 'wt')]) self.assertEqual(2, mock_file.return_value.write.call_count) self.assertEqual(mock_file.return_value.write.call_args_list, [ call('0000:03:00.2'), call('0000:03:00.3')]) @patch('netplan_cli.cli.sriov.PCIDevice.is_pf', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.sriov.PCIDevice.driver', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.commands.sriov_rebind._get_pci_slot_name') @patch('netplan_cli.cli.commands.sriov_rebind.bind_vfs') def test_cli_rebind_mellanox_with_unsupported_bond_mode(self, bind_mock, gpsn, driver_mock, is_pf_mock): with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: embedded-switch-mode: "legacy" delay-virtual-functions-rebind: true enp1s16f1: link: enp1 enp1s16f2: link: enp1 bonds: bond0: parameters: mode: balance-rr interfaces: - enp1 ''', file=fd) gpsn.return_value = '0000:03:00.0' bind_mock.return_value = [] driver_mock.return_value = 'mlx5_core' is_pf_mock.return_value = True out = call_cli(['rebind', '--debug', '--root-dir', self.workdir.name, 'enp1']) self.assertIn('LAG mode balance-rr is not supported by VF LAG', out) @patch('netplan_cli.cli.commands.sriov_rebind.sleep') @patch('netplan_cli.cli.sriov.PCIDevice.is_pf', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.sriov.PCIDevice.driver', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.commands.sriov_rebind._get_pci_slot_name') @patch('netplan_cli.cli.commands.sriov_rebind.bind_vfs') @patch('os.path.exists') def test_cli_rebind_mellanox_state_file_not_found(self, path_mock, bind_mock, gpsn, driver_mock, is_pf_mock, sleep_mock): with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: embedded-switch-mode: "legacy" delay-virtual-functions-rebind: true enp1s16f1: link: enp1 enp1s16f2: link: enp1 bonds: bond0: parameters: mode: active-backup interfaces: - enp1 ''', file=fd) sleep_mock.return_value = None path_mock.return_value = False gpsn.return_value = '0000:03:00.0' bind_mock.return_value = [] driver_mock.return_value = 'mlx5_core' is_pf_mock.return_value = True out = call_cli(['rebind', '--debug', '--root-dir', self.workdir.name, 'enp1']) self.assertIn('VF LAG state debugfs file not found', out) @patch('netplan_cli.cli.commands.sriov_rebind.NetplanSriovRebind._get_mlx5_vf_lag_state') @patch('netplan_cli.cli.commands.sriov_rebind.sleep') @patch('netplan_cli.cli.sriov.PCIDevice.is_pf', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.sriov.PCIDevice.driver', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.commands.sriov_rebind._get_pci_slot_name') @patch('netplan_cli.cli.commands.sriov_rebind.bind_vfs') @patch('os.path.exists') def test_cli_rebind_mellanox_state_file_cannot_be_read(self, path_mock, bind_mock, gpsn, driver_mock, is_pf_mock, sleep_mock, get_mlx5_mock): with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: embedded-switch-mode: "legacy" delay-virtual-functions-rebind: true enp1s16f1: link: enp1 enp1s16f2: link: enp1 bonds: bond0: parameters: mode: active-backup interfaces: - enp1 ''', file=fd) sleep_mock.return_value = None path_mock.return_value = True gpsn.return_value = '0000:03:00.0' bind_mock.return_value = [] driver_mock.return_value = 'mlx5_core' is_pf_mock.return_value = True get_mlx5_mock.side_effect = Exception out = call_cli(['rebind', '--debug', '--root-dir', self.workdir.name, 'enp1']) self.assertIn('VF LAG state cannot be read', out) @patch('netplan_cli.cli.commands.sriov_rebind.NetplanSriovRebind._get_mlx5_vf_lag_state') @patch('netplan_cli.cli.commands.sriov_rebind.sleep') @patch('netplan_cli.cli.sriov.PCIDevice.is_pf', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.sriov.PCIDevice.driver', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.commands.sriov_rebind._get_pci_slot_name') @patch('netplan_cli.cli.commands.sriov_rebind.bind_vfs') @patch('os.path.exists') def test_cli_rebind_mellanox_disabled_after_waiting(self, path_mock, bind_mock, gpsn, driver_mock, is_pf_mock, sleep_mock, get_mlx5_mock): with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: embedded-switch-mode: "legacy" delay-virtual-functions-rebind: true enp1s16f1: link: enp1 enp1s16f2: link: enp1 bonds: bond0: parameters: mode: active-backup interfaces: - enp1 ''', file=fd) sleep_mock.return_value = None path_mock.return_value = True gpsn.return_value = '0000:03:00.0' bind_mock.return_value = [] driver_mock.return_value = 'mlx5_core' is_pf_mock.return_value = True get_mlx5_mock.return_value = 'disabled' out = call_cli(['rebind', '--debug', '--root-dir', self.workdir.name, 'enp1']) self.assertIn('VF LAG state is still \'disabled\' after waiting', out) # check if are we retrying the expected number of times retries = int(MAX_WAITING_TIME_SEC / INTERVAL_SEC) sleep_calls = [call(INTERVAL_SEC)] * retries self.assertEqual(sleep_mock.call_args_list, sleep_calls) @patch('netplan_cli.cli.commands.sriov_rebind.NetplanSriovRebind._get_mlx5_vf_lag_state') @patch('netplan_cli.cli.commands.sriov_rebind.sleep') @patch('netplan_cli.cli.sriov.PCIDevice.is_pf', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.sriov.PCIDevice.driver', new_callable=unittest.mock.PropertyMock) @patch('netplan_cli.cli.commands.sriov_rebind._get_pci_slot_name') @patch('netplan_cli.cli.commands.sriov_rebind.bind_vfs') @patch('os.path.exists') def test_cli_rebind_mellanox_vf_lag_state_is_active(self, path_mock, bind_mock, gpsn, driver_mock, is_pf_mock, sleep_mock, get_mlx5_mock): with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: embedded-switch-mode: "legacy" delay-virtual-functions-rebind: true enp1s16f1: link: enp1 enp1s16f2: link: enp1 bonds: bond0: parameters: mode: active-backup interfaces: - enp1 ''', file=fd) sleep_mock.return_value = None path_mock.return_value = True gpsn.return_value = '0000:03:00.0' bind_mock.return_value = [] driver_mock.return_value = 'mlx5_core' is_pf_mock.return_value = True get_mlx5_mock.return_value = 'active' out = call_cli(['rebind', '--debug', '--root-dir', self.workdir.name, 'enp1']) self.assertIn('VF LAG state is \'active\'', out) def test_get_mlx5_vf_lag_state(self): rebind = NetplanSriovRebind() f = StringIO() f.write(' active') f.seek(0) with patch('builtins.open') as open_mock: open_mock.return_value = f state = rebind._get_mlx5_vf_lag_state('fake_pci_address') self.assertEqual(state, 'active') @patch('subprocess.check_output') def test_PCIDevice_devlink_eswitch_mode_query(self, check_output_mock): pcidev = sriov.PCIDevice('0000:03:00.0') check_output_mock.return_value = '{"dev":{"pci/0000:03:00.0":{"mode":"switchdev"}}}' self.assertEqual(pcidev.devlink_eswitch_mode(), 'switchdev') check_output_mock.assert_has_calls([ call(['/sbin/devlink', '-j', 'dev', 'eswitch', 'show', 'pci/0000:03:00.0'], stderr=-3), ]) @patch('subprocess.check_output') def test_PCIDevice_devlink_eswitch_mode_query_not_supported(self, check_output_mock): pcidev = sriov.PCIDevice('0000:03:00.0') check_output_mock.return_value = '{"dev":{}}' self.assertEqual(pcidev.devlink_eswitch_mode(), '__undetermined') check_output_mock.assert_has_calls([ call(['/sbin/devlink', '-j', 'dev', 'eswitch', 'show', 'pci/0000:03:00.0'], stderr=-3), ]) @patch('subprocess.check_output') def test_PCIDevice_devlink_eswitch_mode_query_failure(self, check_output_mock): pcidev = sriov.PCIDevice('0000:03:00.0') check_output_mock.side_effect = subprocess.CalledProcessError(1, None) self.assertEqual(pcidev.devlink_eswitch_mode(), '__undetermined') check_output_mock.assert_has_calls([ call(['/sbin/devlink', '-j', 'dev', 'eswitch', 'show', 'pci/0000:03:00.0'], stderr=-3), ]) class TestParser(TestBase): def test_eswitch_mode(self): self.generate('''network: version: 2 ethernets: engreen: embedded-switch-mode: switchdev delay-virtual-functions-rebind: true enblue: match: {driver: fake_driver} set-name: enblue embedded-switch-mode: legacy delay-virtual-functions-rebind: true virtual-function-count: 4 sriov_vf0: link: engreen''') self.assert_sriov({'rebind.service': '''[Unit] Description=(Re-)bind SR-IOV Virtual Functions to their driver After=network.target After=netplan-sriov-apply.service After=sys-subsystem-net-devices-enblue.device After=sys-subsystem-net-devices-engreen.device [Service] Type=oneshot ExecStart=/usr/sbin/netplan rebind --debug enblue engreen ''', 'apply.service': '''[Unit] Description=Apply SR-IOV configuration DefaultDependencies=no Before=network-pre.target After=sys-subsystem-net-devices-enblue.device After=sys-subsystem-net-devices-engreen.device [Service] Type=oneshot ExecStart=/usr/sbin/netplan apply --sriov-only '''}) def test_rebind_service_generation(self): self.generate('''network: version: 2 ethernets: engreen: embedded-switch-mode: switchdev delay-virtual-functions-rebind: true enblue: match: {driver: fake_driver} set-name: enblue embedded-switch-mode: legacy delay-virtual-functions-rebind: true virtual-function-count: 4 sriov_blue_vf0: link: enblue sriov_blue_vf1: link: enblue sriov_blue_vf1: link: enblue sriov_green_vf0: link: engreen sriov_green_vf1: link: engreen sriov_green_vf2: link: engreen''') self.assert_sriov({'rebind.service': '''[Unit] Description=(Re-)bind SR-IOV Virtual Functions to their driver After=network.target After=netplan-sriov-apply.service After=sys-subsystem-net-devices-enblue.device After=sys-subsystem-net-devices-engreen.device [Service] Type=oneshot ExecStart=/usr/sbin/netplan rebind --debug enblue engreen ''', 'apply.service': '''[Unit] Description=Apply SR-IOV configuration DefaultDependencies=no Before=network-pre.target After=sys-subsystem-net-devices-enblue.device After=sys-subsystem-net-devices-engreen.device [Service] Type=oneshot ExecStart=/usr/sbin/netplan apply --sriov-only '''}) def test_rebind_not_delayed(self): self.generate('''network: version: 2 ethernets: engreen: embedded-switch-mode: switchdev delay-virtual-functions-rebind: false sriov_vf: link: engreen''') self.assert_sriov({'apply.service': '''[Unit] Description=Apply SR-IOV configuration DefaultDependencies=no Before=network-pre.target After=sys-subsystem-net-devices-engreen.device [Service] Type=oneshot ExecStart=/usr/sbin/netplan apply --sriov-only '''}) def test_rebind_no_iface(self): out = self.generate('''network: version: 2 ethernets: engreen: match: {name: 'enp4f[1-3]'} embedded-switch-mode: switchdev delay-virtual-functions-rebind: true sriov_vf: link: engreen''') self.assert_sriov({'apply.service': '''[Unit] Description=Apply SR-IOV configuration DefaultDependencies=no Before=network-pre.target [Service] Type=oneshot ExecStart=/usr/sbin/netplan apply --sriov-only '''}) self.assertIn('engreen: Cannot rebind SR-IOV virtual functions, unknown interface name.', out) def test_invalid_not_a_pf(self): err = self.generate('''network: version: 2 ethernets: engreen: embedded-switch-mode: legacy''', expect_fail=True) self.assertIn("This is not a SR-IOV PF", err) def test_invalid_eswitch_mode(self): err = self.generate('''network: version: 2 ethernets: engreen: embedded-switch-mode: invalid''', expect_fail=True) self.assertIn("needs to be 'switchdev' or 'legacy'", err) netplan-1.0/tests/test_terminal.py000066400000000000000000000062701457004145200174100ustar00rootroot00000000000000#!/usr/bin/python3 # Validate Terminal handling # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import fcntl import sys import os import termios import unittest import netplan_cli.terminal @unittest.skipUnless(sys.__stdin__.isatty(), "not supported when run from a script") class TestTerminal(unittest.TestCase): def setUp(self): self.terminal = netplan_cli.terminal.Terminal(sys.stdin.fileno()) def test_echo(self): self.terminal.disable_echo() attrs = termios.tcgetattr(self.terminal.fd) self.assertFalse(attrs[3] & termios.ECHO) self.terminal.enable_echo() attrs = termios.tcgetattr(self.terminal.fd) self.assertTrue(attrs[3] & termios.ECHO) def test_nonblocking_io(self): orig_flags = flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) self.assertFalse(flags & os.O_NONBLOCK) self.terminal.enable_nonblocking_io() flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) self.assertTrue(flags & os.O_NONBLOCK) self.assertNotEqual(flags, orig_flags) self.terminal.disable_nonblocking_io() flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) self.assertFalse(flags & os.O_NONBLOCK) self.assertEqual(flags, orig_flags) def test_save(self): self.terminal.enable_nonblocking_io() flags = self.terminal.orig_flags self.terminal.save() self.terminal.disable_nonblocking_io() self.assertNotEqual(flags, self.terminal.orig_flags) self.terminal.reset() flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) self.assertEqual(flags, self.terminal.orig_flags) self.terminal.disable_nonblocking_io() self.terminal.save() def test_save_and_restore_with_dict(self): self.terminal.enable_nonblocking_io() orig_settings = dict() self.terminal.save(orig_settings) self.terminal.disable_nonblocking_io() flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) self.assertNotEqual(flags, orig_settings.get('flags')) self.terminal.reset(orig_settings) flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) self.assertEqual(flags, orig_settings.get('flags')) self.terminal.disable_nonblocking_io() def test_reset(self): self.terminal.enable_nonblocking_io() flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) self.assertTrue(flags & os.O_NONBLOCK) self.terminal.reset() flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) self.assertFalse(flags & os.O_NONBLOCK) netplan-1.0/tests/test_utils.py000066400000000000000000000411341457004145200167330ustar00rootroot00000000000000#!/usr/bin/python3 # # Copyright (C) 2020 Canonical, Ltd. # Author: Lukas Märdian # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import io import os import sys import unittest import tempfile import glob import netifaces import netplan from contextlib import redirect_stdout from netplan_cli.cli.core import Netplan import netplan_cli.cli.utils as utils from unittest.mock import patch DEVICES = ['eth0', 'eth1', 'ens3', 'ens4', 'br0'] # Consider switching to something more standard, like MockProc class MockCmd: """MockCmd will mock a given command name and capture all calls to it""" def __init__(self, name): self._tmp = tempfile.TemporaryDirectory() self.name = name self.path = os.path.join(self._tmp.name, name) self.call_log = os.path.join(self._tmp.name, "call.log") with open(self.path, "w") as fp: fp.write("""#!/bin/bash printf "%%s" "$(basename "$0")" >> %(log)s printf '\\0' >> %(log)s for arg in "$@"; do printf "%%s" "$arg" >> %(log)s printf '\\0' >> %(log)s done printf '\\0' >> %(log)s """ % {'log': self.call_log}) os.chmod(self.path, 0o755) def calls(self): """ calls() returns the calls to the given mock command in the form of [ ["cmd", "call1-arg1"], ["cmd", "call2-arg1"], ... ] """ with open(self.call_log) as fp: b = fp.read() calls = [] for raw_call in b.rstrip("\0\0").split("\0\0"): call = raw_call.rstrip("\0") calls.append(call.split("\0")) return calls def set_output(self, output): with open(self.path, "a") as fp: fp.write("\ncat << EOF\n%s\nEOF" % output) def touch(self, stamp_path): with open(self.path, "a") as fp: fp.write("\ntouch %s\n" % stamp_path) def set_timeout(self, timeout_dsec=10): with open(self.path, "a") as fp: fp.write(""" if [[ "$*" == *try* ]] then ACTIVE=1 trap 'ACTIVE=0' SIGUSR1 trap 'ACTIVE=0' SIGINT while (( $ACTIVE > 0 )) && (( $ACTIVE <= {} )) do ACTIVE=$(($ACTIVE+1)) sleep 0.1 done fi """.format(timeout_dsec)) def set_returncode(self, returncode): with open(self.path, "a") as fp: fp.write("\nexit %d" % returncode) def call_cli(args): old_sys_argv = sys.argv sys.argv = [old_sys_argv[0]] + args f = io.StringIO() try: with redirect_stdout(f): netplan = Netplan() netplan.parse_args() netplan.run_command() return f.getvalue() finally: sys.argv = old_sys_argv class TestUtils(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() self.confdir = os.path.join(self.workdir.name, 'etc/netplan') self.default_conf = os.path.join(self.confdir, 'a.yaml') os.makedirs(self.confdir) os.makedirs(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections')) def load_conf(self, conf_txt): with open(self.default_conf, 'w') as f: f.write(conf_txt) parser = netplan.Parser() parser.load_yaml_hierarchy(rootdir=self.workdir.name) state = netplan.State() state.import_parser_results(parser) return state def _create_nm_keyfile(self, filename, ifname): with open(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/', filename), 'w') as f: f.write('[connection]\n') f.write('key=value\n') f.write('interface-name=%s\n' % ifname) f.write('key2=value2\n') def test_nm_interfaces(self): self._create_nm_keyfile('netplan-test.nmconnection', 'eth0') self._create_nm_keyfile('netplan-test2.nmconnection', 'eth1') ifaces = utils.nm_interfaces(glob.glob(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/*.nmconnection')), DEVICES) self.assertTrue('eth0' in ifaces) self.assertTrue('eth1' in ifaces) self.assertTrue(len(ifaces) == 2) def test_nm_interfaces_globbing(self): self._create_nm_keyfile('netplan-test.nmconnection', 'eth?') ifaces = utils.nm_interfaces(glob.glob(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/*.nmconnection')), DEVICES) self.assertTrue('eth0' in ifaces) self.assertTrue('eth1' in ifaces) self.assertTrue(len(ifaces) == 2) def test_nm_interfaces_globbing2(self): self._create_nm_keyfile('netplan-test.nmconnection', 'e*') ifaces = utils.nm_interfaces(glob.glob(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/*.nmconnection')), DEVICES) self.assertTrue('eth0' in ifaces) self.assertTrue('eth1' in ifaces) self.assertTrue('ens3' in ifaces) self.assertTrue('ens4' in ifaces) self.assertTrue(len(ifaces) == 4) # For the matching tests, we mock out the functions querying extra data @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_find_matching_iface_too_many(self, gim, gidn): gidn.side_effect = lambda x: 'foo' if x == 'ens4' else 'bar' gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'eth1' else '00:00:00:00:00:00' state = self.load_conf('''network: ethernets: netplan-id: match: name: "e*"''') # too many matches iface = utils.find_matching_iface(DEVICES, state['netplan-id']) self.assertEqual(iface, None) @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_find_matching_iface(self, gim, gidn): # we mock-out get_interface_macaddress to return useful values for the test gidn.side_effect = lambda x: 'foo' if x == 'ens4' else 'bar' gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'eth1' else '00:00:00:00:00:00' state = self.load_conf('''network: ethernets: netplan-id: match: name: "e*" macaddress: "00:01:02:03:04:05"''') iface = utils.find_matching_iface(DEVICES, state['netplan-id']) self.assertEqual(iface, 'eth1') @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_find_matching_iface_name_and_driver(self, gim, gidn): gidn.side_effect = lambda x: 'foo' if x == 'ens4' else 'bar' gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'eth1' else '00:00:00:00:00:00' state = self.load_conf('''network: ethernets: netplan-id: match: name: "ens?" driver: "f*"''') iface = utils.find_matching_iface(DEVICES, state['netplan-id']) self.assertEqual(iface, 'ens4') @patch('netplan_cli.cli.utils.get_interface_driver_name') @patch('netplan_cli.cli.utils.get_interface_macaddress') def test_find_matching_iface_name_and_drivers(self, gim, gidn): # we mock-out get_interface_driver_name to return useful values for the test gidn.side_effect = lambda x: 'foo' if x == 'ens4' else 'bar' gim.side_effect = lambda x: '00:01:02:03:04:05' state = self.load_conf('''network: ethernets: netplan-id: match: name: "ens?" driver: ["baz", "f*", "quux"]''') iface = utils.find_matching_iface(DEVICES, state['netplan-id']) self.assertEqual(iface, 'ens4') @patch('netifaces.ifaddresses') def test_interface_macaddress(self, ifaddr): ifaddr.side_effect = lambda _: {netifaces.AF_LINK: [{'addr': '00:01:02:03:04:05'}]} self.assertEqual(utils.get_interface_macaddress('eth42'), '00:01:02:03:04:05') @patch('netifaces.ifaddresses') def test_interface_macaddress_empty(self, ifaddr): ifaddr.side_effect = lambda _: {} self.assertEqual(utils.get_interface_macaddress('eth42'), '') def test_systemctl(self): self.mock_systemctl = MockCmd('systemctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_systemctl.path) + os.pathsep + path_env utils.systemctl('start', ['service1', 'service2']) self.assertEqual(self.mock_systemctl.calls(), [['systemctl', 'start', '--no-block', 'service1', 'service2']]) def test_networkd_interfaces(self): self.mock_networkctl = MockCmd('networkctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_networkctl.path) + os.pathsep + path_env self.mock_networkctl.set_output(''' 1 lo loopback carrier unmanaged 2 ens3 ether routable configured 3 wlan0 wlan routable configuring 174 wwan0 wwan off linger''') res = utils.networkd_interfaces() self.assertEqual(self.mock_networkctl.calls(), [['networkctl', '--no-pager', '--no-legend']]) self.assertIn('2', res) self.assertIn('3', res) def test_networkctl_reload(self): self.mock_networkctl = MockCmd('networkctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_networkctl.path) + os.pathsep + path_env utils.networkctl_reload() self.assertEqual(self.mock_networkctl.calls(), [ ['networkctl', 'reload'] ]) def test_networkctl_reconfigure(self): self.mock_networkctl = MockCmd('networkctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_networkctl.path) + os.pathsep + path_env utils.networkctl_reconfigure(['3', '5']) self.assertEqual(self.mock_networkctl.calls(), [ ['networkctl', 'reconfigure', '3', '5'] ]) def test_is_nm_snap_enabled(self): self.mock_cmd = MockCmd('systemctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env self.assertTrue(utils.is_nm_snap_enabled()) self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', '--quiet', 'is-enabled', 'snap.network-manager.networkmanager.service'] ]) def test_is_nm_snap_enabled_false(self): self.mock_cmd = MockCmd('systemctl') self.mock_cmd.set_returncode(1) path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env self.assertFalse(utils.is_nm_snap_enabled()) self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', '--quiet', 'is-enabled', 'snap.network-manager.networkmanager.service'] ]) def test_systemctl_network_manager(self): self.mock_cmd = MockCmd('systemctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env utils.systemctl_network_manager('start') self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', '--quiet', 'is-enabled', 'snap.network-manager.networkmanager.service'], ['systemctl', 'start', '--no-block', 'snap.network-manager.networkmanager.service'] ]) def test_systemctl_is_active(self): self.mock_cmd = MockCmd('systemctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env self.assertTrue(utils.systemctl_is_active('some.service')) self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', '--quiet', 'is-active', 'some.service'] ]) def test_systemctl_is_active_false(self): self.mock_cmd = MockCmd('systemctl') self.mock_cmd.set_returncode(1) path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env self.assertFalse(utils.systemctl_is_active('some.service')) self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', '--quiet', 'is-active', 'some.service'] ]) def test_systemctl_is_masked(self): self.mock_cmd = MockCmd('systemctl') self.mock_cmd.set_output('masked-runtime') self.mock_cmd.set_returncode(1) path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env self.assertTrue(utils.systemctl_is_masked('some.service')) self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', 'is-enabled', 'some.service'] ]) def test_systemctl_is_masked_false(self): self.mock_cmd = MockCmd('systemctl') self.mock_cmd.set_output('enabled') self.mock_cmd.set_returncode(0) path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env self.assertFalse(utils.systemctl_is_masked('some.service')) self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', 'is-enabled', 'some.service'] ]) def test_systemctl_is_installed(self): self.mock_cmd = MockCmd('systemctl') self.mock_cmd.set_returncode(0) path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env self.assertTrue(utils.systemctl_is_installed('some.service')) self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', 'is-enabled', 'some.service'] ]) def test_systemctl_is_installed_false(self): self.mock_cmd = MockCmd('systemctl') self.mock_cmd.set_returncode(4) path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env self.assertFalse(utils.systemctl_is_installed('some.service')) self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', 'is-enabled', 'some.service'] ]) def test_systemctl_daemon_reload(self): self.mock_cmd = MockCmd('systemctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env utils.systemctl_daemon_reload() self.assertEqual(self.mock_cmd.calls(), [ ['systemctl', 'daemon-reload'] ]) def test_ip_addr_flush(self): self.mock_cmd = MockCmd('ip') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env utils.ip_addr_flush('eth42') self.assertEqual(self.mock_cmd.calls(), [ ['ip', 'addr', 'flush', 'eth42'] ]) @patch('netplan_cli.cli.utils.nmcli_out') def test_nm_get_connection_for_interface(self, nmcli): nmcli.return_value = 'CONNECTION \nlo \n' out = utils.nm_get_connection_for_interface('lo') self.assertEqual(out, 'lo') @patch('netplan_cli.cli.utils.nmcli_out') def test_nm_get_connection_for_interface_no_connection(self, nmcli): nmcli.return_value = 'CONNECTION \n-- \n' out = utils.nm_get_connection_for_interface('asd0') self.assertEqual(out, '') @patch('builtins.open') def test_route_table_lookup(self, open_mock): file = io.StringIO() data = '#\n# reserved values\n#\n255\tlocal\n254\tmain\n253\tdefault\n0\tunspec\n#\n# local\n#\n#1\tinr.ruhep\n' file.write(data) file.seek(0) open_mock.return_value = file expected = {0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255} out = utils.route_table_lookup() self.assertDictEqual(out, expected) @patch('builtins.open') def test_route_table_lookup_fail(self, open_mock): open_mock.side_effect = Exception out = utils.route_table_lookup() self.assertDictEqual(out, {0: 'unspec', 253: 'default', 254: 'main', 255: 'local', 'unspec': 0, 'default': 253, 'main': 254, 'local': 255}) netplan-1.0/tests/utils.py000066400000000000000000000005501457004145200156710ustar00rootroot00000000000000import os import netplan def state_from_yaml(confdir, yaml, filename="a.yml"): os.makedirs(confdir, exist_ok=True) conf = os.path.join(confdir, filename) with open(conf, "w+") as f: f.write(yaml) parser = netplan.Parser() parser.load_yaml(conf) state = netplan.State() state.import_parser_results(parser) return state netplan-1.0/tests/validate_docs.sh000077500000000000000000000033251457004145200173220ustar00rootroot00000000000000#!/bin/bash # find everything that looks like # {"driver", YAML_SCALAR_NODE,..., # extract the thing in quotes. # coherence check: make sure none have disappeared, as might happen from a reformat. count=$(sed -n 's/[ ]\+{"\([a-z0-9-]\+\)", YAML_[A-Z]\+_NODE.*/\1/p' src/parse.c | sort | wc -l) # 144 is based on 0.99+da6f776 definitions, and should be updated periodically. if [ $count -lt 202 ]; then echo "ERROR: fewer YAML keys defined in src/parse.c than expected!" echo " Has the file been reformatted or refactored? If so, modify" echo " validate_docs.sh appropriately." exit 1 fi # iterate through the keys for term in $(sed -n 's/[ ]\+{"\([a-z0-9-]\+\)", YAML_[A-Z]\+_NODE.*/\1/p' src/parse.c | sort | uniq); do # it can be documented in the following ways. # 1. "Properties for device type `blah:` if egrep "## Properties for device type \`$term:\`" doc/netplan-yaml.md > /dev/null; then continue fi # 2. "[blah, ]**blah**[, **blah2**]: (scalar|bool|...) if egrep "Alias: \*\*\`$term\`\*\*|\*\*\`$term\`\*\*.*\((scalar|boolean|mapping|sequence of scalars|sequence of mappings|sequence of sequence of scalars)" doc/netplan-yaml.md > /dev/null; then continue fi # 3. we give a pass to network and version if [[ $term = "network" ]] || [[ $term = "version" ]]; then continue fi # 4. search doesn't get a full description but it's good enough if [[ $term = "search" ]]; then continue fi # 5. gratuit_i_ous arp gets a special note if [[ $term = "gratuitious-arp" ]]; then continue fi echo ERROR: The key "$term" is defined in the parser but not documented. exit 1 done echo "validate_docs: OK" netplan-1.0/tools/000077500000000000000000000000001457004145200141555ustar00rootroot00000000000000netplan-1.0/tools/completely.yaml000066400000000000000000000016451457004145200172240ustar00rootroot00000000000000# Used to regenerate the bash auto completion script # with https://github.com/DannyBen/completely netplan: - -h - --help - --debug - help - apply - generate - get - info - ip - set - rebind - status - try netplan help: - -h - --help netplan apply: - -h - --help - --debug - --sriov-only - --only-ovs-cleanup - --state netplan generate: - -h - --help - --debug - --root-dir - --mapping netplan get: - -h - --help - --debug - --root-dir netplan info: - -h - --help - --debug - --json - --yaml netplan ip: - -h - --help - --debug - help - leases netplan ip leases: - -h - --help - --debug - --root-dir netplan set: - -h - --help - --debug - --origin-hint netplan rebind: - -h - --help - --debug netplan status: - -h - --help - --debug - -a - --all - --diff - --diff-only - --root-dir - --verbose - -f - --format - $(ls /sys/class/net 2> /dev/null) netplan try: - -h - --help - --debug - --config-file - --timeout - --state netplan-1.0/tools/keyfile_to_yaml.c000066400000000000000000000012641457004145200175000ustar00rootroot00000000000000#include #include #include #include #include #include int main(int argc, char** argv) { NetplanParser *npp; NetplanState *np_state; NetplanError* error = NULL; if (argc < 2) return 1; npp = netplan_parser_new(); netplan_parser_load_keyfile(npp, argv[1], &error); if (error) goto exit_parser; np_state = netplan_state_new(); netplan_state_import_parser_results(np_state, npp, &error); if (!error) printf("keyfile %s loaded\n", argv[1]); netplan_state_clear(&np_state); exit_parser: netplan_parser_clear(&npp); if (error) netplan_error_clear(&error); return 0; } netplan-1.0/tools/keyfile_to_yaml.py000066400000000000000000000011211457004145200176760ustar00rootroot00000000000000# A simple tool to convert a Network Manager keyfile to Netplan YAML # How to use: # From the Netplan source directory, run: # PYTHONPATH=. python3 tools/keyfile_to_yaml.py path/to/the/file.nmconnection import io import sys import netplan if len(sys.argv) < 2: print("Pass the NM keyfile as parameter") sys.exit(1) parser = netplan.Parser() state = netplan.State() try: parser.load_keyfile(sys.argv[1]) state.import_parser_results(parser) except Exception as e: print(e) sys.exit(1) output = io.StringIO() state._dump_yaml(output) print(output.getvalue()) netplan-1.0/tools/meson-make-symlink.sh000077500000000000000000000004701457004145200202350ustar00rootroot00000000000000#!/bin/sh set -eu # this is needed mostly because $DESTDIR is provided as a variable, # and we need to create the target directory... mkdir -vp "$(dirname "${DESTDIR:-}$2")" if [ "$(dirname $1)" = . ]; then ln -fs -T "$1" "${DESTDIR:-}$2" else ln -fs -T --relative "${DESTDIR:-}$1" "${DESTDIR:-}$2" fi netplan-1.0/tools/run_asan.sh000077500000000000000000000046221457004145200163260ustar00rootroot00000000000000#!/bin/bash set -e set -x BUILDDIR="_leakcheckbuild" CLEANBUILDDIR="_cleanbuild" CC=gcc meson setup ${BUILDDIR} -Db_sanitize=address,undefined meson compile -C ${BUILDDIR} --verbose meson setup ${CLEANBUILDDIR} meson compile -C ${CLEANBUILDDIR} --verbose ${CC} tools/keyfile_to_yaml.c -o tools/keyfile_to_yaml \ -lnetplan $(pkg-config --cflags --libs glib-2.0) \ -Iinclude -L${BUILDDIR}/src \ -fsanitize=address,undefined -g TESTS=$(find ${BUILDDIR}/tests/ctests/ -executable -type f) for test in ${TESTS} do ./${test} done mkdir -p ${BUILDDIR}/fakeroot/{etc/netplan,run} for yaml in examples/*.yaml do filepath=${BUILDDIR}/fakeroot/etc/netplan/${yaml##*/} filename=$(basename ${filepath}) cp ${yaml} ${BUILDDIR}/fakeroot/etc/netplan/ chmod 600 ${filepath} # Set the renderer and check if the new file can be parsed with the new renderer # We use the clean build because it will not fail if there's a memory leak PYTHONPATH=".:${CLEANBUILDDIR}/python-cffi" LD_LIBRARY_PATH="${CLEANBUILDDIR}/src" src/netplan.script set --root-dir ${BUILDDIR}/fakeroot --origin-hint ${filename/.yaml/} network.renderer=networkd if PYTHONPATH=".:${CLEANBUILDDIR}/python-cffi" LD_LIBRARY_PATH="${CLEANBUILDDIR}/src" src/netplan.script generate --root-dir ${BUILDDIR}/fakeroot > /dev/null 2>&1 then LD_LIBRARY_PATH="${BUILDDIR}/src" ./${BUILDDIR}/src/generate --root-dir ${BUILDDIR}/fakeroot else echo "File ${filename} can't be parsed with renderer = networkd" fi PYTHONPATH=".:${CLEANBUILDDIR}/python-cffi" LD_LIBRARY_PATH="${CLEANBUILDDIR}/src" src/netplan.script set --root-dir ${BUILDDIR}/fakeroot --origin-hint ${filename/.yaml/} network.renderer=NetworkManager if PYTHONPATH=".:${CLEANBUILDDIR}/python-cffi" LD_LIBRARY_PATH="${CLEANBUILDDIR}/src" src/netplan.script generate --root-dir ${BUILDDIR}/fakeroot > /dev/null 2>&1 then LD_LIBRARY_PATH="${BUILDDIR}/src" ./${BUILDDIR}/src/generate --root-dir ${BUILDDIR}/fakeroot for keyfile in $(find ${BUILDDIR}/fakeroot/run/NetworkManager/system-connections/ -type f) do sed -i 's/\[connection\]/\[connection\]\nuuid=c87fb5fc-f607-45f3-8fcd-720b83a742e4/' "${keyfile}" LD_LIBRARY_PATH="${BUILDDIR}/src" ./tools/keyfile_to_yaml "${keyfile}" done else echo "File ${filename} can't be parsed with renderer = NetworkManager" fi rm ${filepath} done