diff --git a/.github/workflows/autopkgtest.yml b/.github/workflows/autopkgtest.yml index a1b430f01..06e953086 100644 --- a/.github/workflows/autopkgtest.yml +++ b/.github/workflows/autopkgtest.yml @@ -42,6 +42,7 @@ jobs: pull-lp-source netplan.io cp -r netplan.io-*/debian . rm -r debian/patches/ # clear any distro patches + echo "usr/lib/python3/dist-packages/netplan/*" >> debian/netplan.io.install # bindings 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" @@ -50,4 +51,4 @@ jobs: run: | # using --setup-commands temporarily to install: # cmocka/pytest/rich/ethtool until they become proper test-deps - autopkgtest . --setup-commands='apt -y install ethtool python3-rich python3-pytest python3-pytest-cov libcmocka-dev' -U --env=DPKG_GENSYMBOLS_CHECK_LEVEL=0 --env=DEB_BUILD_OPTIONS=nocheck -- lxd autopkgtest/ubuntu/jammy/amd64 + autopkgtest . --setup-commands='apt -y install ethtool python3-rich python3-pytest python3-pytest-cov python3-cffi libpython3-dev libcmocka-dev' -U --env=DPKG_GENSYMBOLS_CHECK_LEVEL=0 --env=DEB_BUILD_OPTIONS=nocheck -- lxd autopkgtest/ubuntu/jammy/amd64 diff --git a/.github/workflows/build-abi.yml b/.github/workflows/build-abi.yml index d2ddc1f41..b0886fc87 100644 --- a/.github/workflows/build-abi.yml +++ b/.github/workflows/build-abi.yml @@ -28,7 +28,7 @@ jobs: sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list sudo apt update #sudo apt install lcov python3-coverage curl - sudo apt install abigail-tools meson python3-coverage python3-pytest python3-pytest-cov + sudo apt install abigail-tools meson python3-coverage python3-pytest python3-pytest-cov python3-cffi libpython3-dev sudo apt build-dep netplan.io # Runs the build diff --git a/.github/workflows/check-address-sanitizer.yml b/.github/workflows/check-address-sanitizer.yml index 1a2ce390d..f65a5a442 100644 --- a/.github/workflows/check-address-sanitizer.yml +++ b/.github/workflows/check-address-sanitizer.yml @@ -23,7 +23,7 @@ jobs: echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list sudo apt update - sudo apt -y install python3-rich python3-coverage python3-pytest python3-pytest-cov curl meson gcovr expect libcmocka-dev + sudo apt -y install python3-rich python3-coverage python3-pytest python3-pytest-cov curl meson gcovr expect libcmocka-dev python3-cffi libpython3-dev sudo apt -y build-dep netplan.io - name: Run unit tests diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 360089bbc..b06e87abf 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -28,15 +28,17 @@ jobs: echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list sudo apt update - sudo apt install python3-rich python3-coverage python3-pytest python3-pytest-cov curl meson gcovr expect libcmocka-dev + sudo apt install python3-rich python3-coverage python3-pytest python3-pytest-cov curl meson gcovr expect libcmocka-dev python3-cffi libpython3-dev sudo apt build-dep 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 --prefix=/usr -Db_coverage=true -Dunit_testing=true - meson compile -C _build - unbuffer meson test -C _build --verbose + 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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 310c6a018..104af8754 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -52,7 +52,7 @@ jobs: 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 + 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). diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index ad40408a4..cd32c0692 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -16,7 +16,7 @@ jobs: sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list sudo apt update sudo apt -y build-dep netplan.io - sudo apt -y install libcmocka-dev meson python3-pytest curl + sudo apt -y install libcmocka-dev meson python3-pytest curl python3-cffi libpython3-dev - 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" diff --git a/.github/workflows/debci.yml b/.github/workflows/debci.yml index 1ce3e8d48..3c5f91afe 100644 --- a/.github/workflows/debci.yml +++ b/.github/workflows/debci.yml @@ -46,6 +46,9 @@ jobs: 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 + echo "usr/lib/python3/dist-packages/netplan/*" >> debian/netplan.io.install # bindings + echo "override_dh_auto_configure:" >> debian/rules + echo " dh_auto_configure -- -Dpython.purelibdir=/usr/lib/python3/dist-packages -Dpython.platlibdir=/usr/lib/python3/dist-packages" >> 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" @@ -54,4 +57,4 @@ jobs: 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) + sudo autopkgtest . -U --env=DPKG_GENSYMBOLS_CHECK_LEVEL=0 --env=DEB_BUILD_OPTIONS=nocheck --setup-commands='apt -y install python3-cffi libpython3-dev' -- lxc autopkgtest-testing-amd64 || test $? -eq 2 # allow OVS test to be skipped (exit code = 2) diff --git a/.github/workflows/network-manager.yml b/.github/workflows/network-manager.yml index 410bdf0f7..e3fb1b33a 100644 --- a/.github/workflows/network-manager.yml +++ b/.github/workflows/network-manager.yml @@ -17,7 +17,7 @@ jobs: # 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 + - uses: actions/checkout@v3 # Setup LXD + Docker fixes - uses: canonical/setup-lxd@v0.1.1 with: @@ -32,24 +32,35 @@ jobs: sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list 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:mantic - name: Prepare test run: | pull-lp-source netplan.io cp -r netplan.io-*/debian . rm -r debian/patches/ # clear any distro patches + echo "3.0 (native)" > debian/source/format # force native build + echo "usr/lib/python3/dist-packages/netplan/*" >> debian/netplan.io.install # bindings + echo "override_dh_auto_configure:" >> debian/rules + echo " dh_auto_configure -- -Dpython.purelibdir=/usr/lib/python3/dist-packages -Dpython.platlibdir=/usr/lib/python3/dist-packages" >> 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" - sudo apt -y build-dep ./ - DEB_BUILD_OPTIONS=nocheck DPKG_GENSYMBOLS_CHECK_LEVEL=0 dpkg-buildpackage -b + # Build deb + - uses: jtdor/build-deb-action@v1 + env: + DEB_BUILD_OPTIONS: nocheck + DPKG_GENSYMBOLS_CHECK_LEVEL: 0 + with: + docker-image: ubuntu:mantic + 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 + autopkgtest-build-lxd ubuntu-daily:mantic - name: Run autopkgtest run: | # using --setup-commands temporarily to install: # cmocka/pytest/rich/ethtool until they become proper test-deps - autopkgtest -U ../*.deb network-manager --apt-pocket=proposed=src:network-manager -- lxd autopkgtest/ubuntu/mantic/amd64 || test $? -eq 2 # allow for skipped tests (exit code = 2) + sudo autopkgtest -U debian/artifacts/*.deb network-manager --apt-pocket=proposed=src:network-manager -- lxd autopkgtest/ubuntu/mantic/amd64 || test $? -eq 2 # allow for skipped tests (exit code = 2) diff --git a/Makefile b/Makefile index 6426ce753..cce4ae56a 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ clean: rm -rf _build-cov rm -rf _leakcheckbuild rm -rf tmproot + rm -f python-cffi/netplan/_netplan_cffi.* check: default meson test -C _build --verbose @@ -38,7 +39,7 @@ pre-coverage: _build-cov meson compile -C _build-cov --verbose check-coverage: pre-coverage - meson test -C _build-cov + meson test -C _build-cov --verbose install: default meson install -C _build --destdir $(DESTDIR) diff --git a/examples/cffi-bindings.py b/examples/cffi-bindings.py new file mode 100644 index 000000000..79613e338 --- /dev/null +++ b/examples/cffi-bindings.py @@ -0,0 +1,69 @@ +#!/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='/') diff --git a/gcovr.cfg b/gcovr.cfg new file mode 100644 index 000000000..94ff134d8 --- /dev/null +++ b/gcovr.cfg @@ -0,0 +1,2 @@ +filter = src/* +filter = tests/ctests/* diff --git a/include/parse-nm.h b/include/parse-nm.h index a7a0cb4ba..7294b30bc 100644 --- a/include/parse-nm.h +++ b/include/parse-nm.h @@ -23,6 +23,10 @@ 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); + /********** Old API below this ***********/ NETPLAN_PUBLIC gboolean diff --git a/include/parse.h b/include/parse.h index b96f45931..e26b7f2c0 100644 --- a/include/parse.h +++ b/include/parse.h @@ -38,6 +38,9 @@ netplan_parser_load_yaml(NetplanParser* npp, const char* filename, NetplanError* NETPLAN_PUBLIC gboolean netplan_parser_load_yaml_from_fd(NetplanParser* npp, int input_fd, NetplanError** error); +NETPLAN_PUBLIC gboolean +netplan_parser_load_yaml_hierarchy(NetplanParser* npp, const char* rootdir, NetplanError** error); + NETPLAN_PUBLIC gboolean netplan_parser_load_nullable_fields(NetplanParser* npp, int input_fd, NetplanError** error); @@ -51,7 +54,7 @@ netplan_state_import_parser_results(NetplanState* np_state, NetplanParser* npp, * only. */ NETPLAN_PUBLIC gboolean netplan_parser_load_nullable_overrides( - NetplanParser* npp, int input_fd, const char* constraint, GError** error); + NetplanParser* npp, int input_fd, const char* constraint, NetplanError** error); /********** Old API below this ***********/ diff --git a/include/types.h b/include/types.h index f2cefa066..5b39fcbb2 100644 --- a/include/types.h +++ b/include/types.h @@ -87,7 +87,7 @@ struct _NetplanStateIterator { * Errors and error domains * * NOTE: if new errors or domains are added, - * netplan/libnetplan.py must be updated with the new entries. + * python-cffi/netplan/_utils.py must be updated with the new entries. */ enum NETPLAN_ERROR_DOMAINS { diff --git a/include/util.h b/include/util.h index c495ce1a7..b3e11fca9 100644 --- a/include/util.h +++ b/include/util.h @@ -54,6 +54,12 @@ netplan_state_iterator_next(NetplanStateIterator* iter); NETPLAN_PUBLIC gboolean netplan_state_iterator_has_next(const NetplanStateIterator* iter); +NETPLAN_PUBLIC gboolean +netplan_util_create_yaml_patch(const char* conf_obj_path, const char* obj_payload, int out_fd, NetplanError** error); + +NETPLAN_PUBLIC gboolean +netplan_util_dump_yaml_subtree(const char* prefix, int input_fd, int output_fd, NetplanError** error); + /********** Old API below this ***********/ NETPLAN_DEPRECATED NETPLAN_PUBLIC gchar* diff --git a/meson.build b/meson.build index da97ae0a6..059c45e26 100644 --- a/meson.build +++ b/meson.build @@ -36,10 +36,12 @@ add_project_arguments( 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') @@ -60,7 +62,7 @@ install_data( # Testing # ########### test_env = [ - 'PYTHONPATH=' + meson.current_source_dir(), + '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'), @@ -107,12 +109,18 @@ if get_option('b_coverage') 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')], diff --git a/netplan_cli/cli/commands/apply.py b/netplan_cli/cli/commands/apply.py index 59bef4fb9..8bf8a9477 100644 --- a/netplan_cli/cli/commands/apply.py +++ b/netplan_cli/cli/commands/apply.py @@ -368,7 +368,7 @@ def process_link_changes(interfaces, config_manager: ConfigManager): # pragma: newname = netdef.set_name if not newname: continue # Skip if no new name needs to be set - if not netdef.has_match: + 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)) diff --git a/netplan_cli/cli/commands/set.py b/netplan_cli/cli/commands/set.py index 2e6ba634b..4b8bdb1ea 100644 --- a/netplan_cli/cli/commands/set.py +++ b/netplan_cli/cli/commands/set.py @@ -22,7 +22,7 @@ import io from ..utils import NetplanCommand -from ... import libnetplan +import netplan FALLBACK_FILENAME = '70-netplan-set.yaml' GLOBAL_KEYS = ['renderer', 'version'] @@ -67,9 +67,9 @@ def command_set(self): # 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) + 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 @@ -122,8 +122,8 @@ def command_set(self): # 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 = libnetplan.State() + state_output_file = netplan.State() state_output_file.import_parser_results(parser_output_file) - state_output_file.write_yaml_file(filename, self.root_dir) + state_output_file._write_yaml_file(filename, self.root_dir) else: - state.update_yaml_hierarchy(FALLBACK_FILENAME, self.root_dir) + state._update_yaml_hierarchy(FALLBACK_FILENAME, self.root_dir) diff --git a/netplan_cli/cli/commands/try_command.py b/netplan_cli/cli/commands/try_command.py index 0b6f81df9..ba3e38269 100644 --- a/netplan_cli/cli/commands/try_command.py +++ b/netplan_cli/cli/commands/try_command.py @@ -18,6 +18,7 @@ '''netplan try command line''' import logging +import netplan import os import time import shutil @@ -29,7 +30,6 @@ from .. import utils from .apply import NetplanApply from ... import terminal -from ... import libnetplan # 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. @@ -179,11 +179,11 @@ def is_revertable(self): # 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 = {} # type: dict[str, libnetplan.NetDefinition] + multi_iface = {} # type: 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: + if not itf._is_trivial_compound_itf: reason = "reverting custom parameters for bridges and bonds is not supported" revert_unsupported.append((itf.id, reason)) diff --git a/netplan_cli/cli/core.py b/netplan_cli/cli/core.py index f2e673616..4a863c3bb 100644 --- a/netplan_cli/cli/core.py +++ b/netplan_cli/cli/core.py @@ -22,7 +22,7 @@ import os from . import utils -from ..libnetplan import NetplanException, NetplanValidationException, NetplanParserException +from netplan import NetplanException, NetplanValidationException, NetplanParserException FALLBACK_PATH = '/usr/bin:/snap/bin' diff --git a/netplan_cli/cli/ovs.py b/netplan_cli/cli/ovs.py index dde682000..0ba0482b6 100644 --- a/netplan_cli/cli/ovs.py +++ b/netplan_cli/cli/ovs.py @@ -130,8 +130,8 @@ def apply_ovs_cleanup(config_manager, ovs_old, ovs_current): # pragma: nocover config_manager.parse() ovs_ifaces = set() - for i in config_manager.all_defs.keys(): - if (is_ovs_interface(i, config_manager.all_defs)): + for i in config_manager.netdefs.keys(): + if (is_ovs_interface(i, config_manager.netdefs)): ovs_ifaces.add(i) # Tear down old OVS interfaces, not defined in the current config. diff --git a/netplan_cli/cli/sriov.py b/netplan_cli/cli/sriov.py index fbe4f5df2..dbec83142 100644 --- a/netplan_cli/cli/sriov.py +++ b/netplan_cli/cli/sriov.py @@ -24,8 +24,8 @@ from collections import defaultdict from . import utils -from .. import libnetplan from ..configmanager import ConfigurationError +import netplan import netifaces @@ -186,7 +186,7 @@ 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: + 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: @@ -196,10 +196,10 @@ def _get_target_interface(interfaces, np_state, pf_link, pfs): pfs[pf_link] = set_name else: for interface in interfaces: - if not pf_dev.match_interface( - itf_name=interface, - itf_driver=utils.get_interface_driver_name(interface), - itf_mac=utils.get_interface_macaddress(interface)): + 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 @@ -240,12 +240,12 @@ def get_vf_count_and_functions(interfaces, np_state, Count how many VFs each PF will need. """ for nid, netdef in np_state.ethernets.items(): - if netdef.sriov_link and _get_target_interface(interfaces, np_state, netdef.sriov_link.id, pfs): + 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 libnetplan.NetplanException as e: + count = netdef._vf_count + except netplan.NetplanException as e: raise ConfigurationError(str(e)) if count == 0: continue @@ -375,10 +375,10 @@ 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. """ - parser = libnetplan.Parser() + parser = netplan.Parser() parser.load_yaml_hierarchy(rootdir) - np_state = libnetplan.State() + np_state = netplan.State() np_state.import_parser_results(parser) config_manager.parse() @@ -426,13 +426,13 @@ def apply_sriov_config(config_manager, rootdir='/'): # XXX: does matching those even make sense? for vf in vfs: netdef = np_state[vf] - if netdef.has_match: + 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(itf_name=interface): + 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 @@ -443,14 +443,14 @@ def apply_sriov_config(config_manager, rootdir='/'): # 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 + eswitch_mode = netdef._embedded_switch_mode if eswitch_mode in ['switchdev', 'legacy']: pci_addr = _get_pci_slot_name(iface) pcidev = PCIDevice(pci_addr) if pcidev.is_pf: logging.debug("Found VFs of {}: {}".format(pcidev, pcidev.vf_addrs)) if pcidev.vfs: - rebind_delayed = netdef.delay_virtual_functions_rebind + rebind_delayed = netdef._delay_virtual_functions_rebind try: unbind_vfs(pcidev.vfs, pcidev.driver) pcidev.devlink_set('eswitch', 'mode', eswitch_mode) @@ -462,10 +462,10 @@ def apply_sriov_config(config_manager, rootdir='/'): 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: + if netdef._has_sriov_vlan_filter: # this only works for SR-IOV VF interfaces - link = netdef.vlan_link - vlan_id = netdef.vlan_id + link = netdef.links.get('vlan') + vlan_id = netdef._vlan_id vf = vfs.get(link.id) if not vf: @@ -479,7 +479,7 @@ def apply_sriov_config(config_manager, rootdir='/'): # get the parent pf interface # first we fetch the related vf netplan entry # and finally, get the matched pf interface - pf = pfs.get(link.sriov_link.id) + pf = pfs.get(link.links.get('sriov').id) if vf in filtered_vlans_set: raise ConfigurationError( diff --git a/netplan_cli/cli/state.py b/netplan_cli/cli/state.py index 0c725b8bc..019dbc208 100644 --- a/netplan_cli/cli/state.py +++ b/netplan_cli/cli/state.py @@ -30,9 +30,9 @@ import yaml import dbus +import netplan from . import utils -from .. import libnetplan JSON = Union[Dict[str, 'JSON'], List['JSON'], int, str, float, bool, Type[None]] @@ -481,25 +481,27 @@ class NetplanConfigState(): def __init__(self, subtree='all', rootdir='/'): - parser = libnetplan.Parser() + parser = netplan.Parser() parser.load_yaml_hierarchy(rootdir) - np_state = libnetplan.State() + np_state = netplan.State() np_state.import_parser_results(parser) self.state = StringIO() if subtree == 'all': - np_state.dump_yaml(output_file=self.state) + np_state._dump_yaml(output_file=self.state) else: if not subtree.startswith('network'): subtree = '.'.join(('network', subtree)) - # Replace the '.' with '\t' but not at '\.' via negative lookbehind expression - subtree = re.sub(r'(? str: return self.state.getvalue() diff --git a/netplan_cli/cli/utils.py b/netplan_cli/cli/utils.py index 1449ee192..d0a85dffc 100644 --- a/netplan_cli/cli/utils.py +++ b/netplan_cli/cli/utils.py @@ -24,9 +24,8 @@ import fnmatch import re -from .. import libnetplan as np from ..configmanager import ConfigurationError -from ..libnetplan import NetplanException +from netplan import NetDefinition, NetplanException NM_SERVICE_NAME = 'NetworkManager.service' @@ -175,13 +174,13 @@ def get_interface_macaddress(interface): def find_matching_iface(interfaces: list, netdef): - assert isinstance(netdef, np.NetDefinition) - assert netdef.has_match + assert isinstance(netdef, NetDefinition) + assert netdef._has_match - matches = list(filter(lambda itf: netdef.match_interface( - itf_name=itf, - itf_driver=get_interface_driver_name(itf), - itf_mac=get_interface_macaddress(itf)), interfaces)) + 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: diff --git a/netplan_cli/configmanager.py b/netplan_cli/configmanager.py index ac7c6fee3..59e8b88dc 100644 --- a/netplan_cli/configmanager.py +++ b/netplan_cli/configmanager.py @@ -18,6 +18,7 @@ '''netplan configuration manager''' import logging +import netplan import os import shutil import sys @@ -25,8 +26,6 @@ from typing import Optional -from . import libnetplan - class ConfigManager(object): def __init__(self, prefix="/", extra_files={}): @@ -36,7 +35,7 @@ def __init__(self, prefix="/", extra_files={}): self.temp_run = os.path.join(self.tempdir, "run") self.extra_files = extra_files self.new_interfaces = set() - self.np_state: Optional[libnetplan.State] = None + 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." @@ -58,7 +57,9 @@ def virtual_interfaces(self): # 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 @@ -72,7 +73,7 @@ def parse(self, extra_config=None): """ # /run/netplan shadows /etc/netplan/, which shadows /lib/netplan - parser = libnetplan.Parser() + parser = netplan.Parser() try: parser.load_yaml_hierarchy(rootdir=self.prefix) @@ -80,12 +81,16 @@ def parse(self, extra_config=None): for f in extra_config: parser.load_yaml(f) - self.np_state = libnetplan.State() + self.np_state = netplan.State() self.np_state.import_parser_results(parser) - except libnetplan.NetplanException as e: + except netplan.NetplanException as e: raise ConfigurationError(str(e)) - self.np_state.dump_to_logs() + # 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): diff --git a/netplan_cli/libnetplan.py b/netplan_cli/libnetplan.py deleted file mode 100644 index 13b5205e9..000000000 --- a/netplan_cli/libnetplan.py +++ /dev/null @@ -1,774 +0,0 @@ -# Copyright (C) 2018-2020 Canonical, Ltd. -# Author: Mathieu Trudel-Lapierre -# Author: Łukasz 'sil2100' Zemczak -# Author: Lukas 'slyon' Märdian -# 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 . - -import tempfile -import logging -from collections import defaultdict -import ctypes -import ctypes.util -from ctypes import c_char_p, c_void_p, c_int, c_uint, c_size_t, c_ssize_t -from typing import List, Union, IO -from enum import IntEnum -from io import StringIO -import os -import re - - -# 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): - pass - - -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, - } - }) - - -class _NetplanError(ctypes.Structure): - _fields_ = [("domain", ctypes.c_uint32), ("code", c_int), ("message", c_char_p)] - - -class _netplan_state(ctypes.Structure): - pass - - -class _netplan_parser(ctypes.Structure): - pass - - -class _netplan_net_definition(ctypes.Structure): - pass - - -class _NetplanAddress(ctypes.Structure): - _fields_ = [("address", c_char_p), ("lifetime", c_char_p), ("label", c_char_p)] - - -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 - - -lib = ctypes.CDLL(ctypes.util.find_library('netplan')) - -_NetplanErrorPP = ctypes.POINTER(ctypes.POINTER(_NetplanError)) -_NetplanParserP = ctypes.POINTER(_netplan_parser) -_NetplanStateP = ctypes.POINTER(_netplan_state) -_NetplanNetDefinitionP = ctypes.POINTER(_netplan_net_definition) -_NetplanAddressP = ctypes.POINTER(_NetplanAddress) - -lib.netplan_get_id_from_nm_filename.restype = ctypes.c_char_p - - -def _string_realloc_call_no_error(function): - size = 16 - while size < 1048576: # 1MB - buffer = ctypes.create_string_buffer(size) - code = function(buffer) - 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 buffer.value.decode('utf-8') - raise NetplanException('Halting due to string buffer size > 1M') # pragma: nocover - - -def _checked_lib_call(fn, *args): - err = ctypes.POINTER(_NetplanError)() - ret = bool(fn(*args, ctypes.byref(err))) - if not ret: - error_domain = err.contents.domain - error_code = err.contents.code - error_message = err.contents.message.decode('utf-8') - exception = NETPLAN_EXCEPTIONS[error_domain][error_code] - raise exception(error_message, error_domain, error_code) - - -class Parser: - _abi_loaded = False - - @classmethod - def _load_abi(cls): - if cls._abi_loaded: - return - - lib.netplan_parser_new.restype = _NetplanParserP - lib.netplan_parser_clear.argtypes = [ctypes.POINTER(_NetplanParserP)] - - lib.netplan_parser_load_yaml.argtypes = [_NetplanParserP, c_char_p, _NetplanErrorPP] - lib.netplan_parser_load_yaml.restype = c_int - - lib.netplan_parser_load_yaml_from_fd.argtypes = [_NetplanParserP, c_int, _NetplanErrorPP] - lib.netplan_parser_load_yaml_from_fd.restype = c_int - - lib.netplan_parser_load_nullable_fields.argtypes = [_NetplanParserP, c_int, _NetplanErrorPP] - lib.netplan_parser_load_nullable_fields.restype = c_int - - lib.netplan_parser_load_nullable_overrides.argtypes =\ - [_NetplanParserP, c_int, c_char_p, _NetplanErrorPP] - lib.netplan_parser_load_nullable_overrides.restype = c_int - - lib.netplan_parser_load_keyfile.argtypes = [_NetplanParserP, c_char_p, _NetplanErrorPP] - lib.netplan_parser_load_keyfile.restype = c_int - - cls._abi_loaded = True - - def __init__(self): - self._load_abi() - self._ptr = lib.netplan_parser_new() - - def __del__(self): - lib.netplan_parser_clear(ctypes.byref(self._ptr)) - - def load_yaml(self, input_file: Union[str, IO]): - if isinstance(input_file, str): - _checked_lib_call(lib.netplan_parser_load_yaml, self._ptr, input_file.encode('utf-8')) - else: - _checked_lib_call(lib.netplan_parser_load_yaml_from_fd, self._ptr, input_file.fileno()) - - def load_yaml_hierarchy(self, rootdir): - _checked_lib_call(lib.netplan_parser_load_yaml_hierarchy, self._ptr, rootdir.encode('utf-8')) - - def load_nullable_fields(self, input_file: IO): - _checked_lib_call(lib.netplan_parser_load_nullable_fields, self._ptr, input_file.fileno()) - - def load_nullable_overrides(self, input_file: IO, constraint: str): - _checked_lib_call(lib.netplan_parser_load_nullable_overrides, - self._ptr, input_file.fileno(), constraint.encode('utf-8')) - - def load_keyfile(self, input_file: str): - _checked_lib_call(lib.netplan_parser_load_keyfile, self._ptr, input_file.encode('utf-8')) - - -class State: - _abi_loaded = False - - @classmethod - def _load_abi(cls): - if cls._abi_loaded: - return - - lib.netplan_state_new.restype = _NetplanStateP - lib.netplan_state_clear.argtypes = [ctypes.POINTER(_NetplanStateP)] - - lib.netplan_state_import_parser_results.argtypes = [_NetplanStateP, _NetplanParserP, _NetplanErrorPP] - lib.netplan_state_import_parser_results.restype = c_int - - lib.netplan_state_get_netdefs_size.argtypes = [_NetplanStateP] - lib.netplan_state_get_netdefs_size.restype = c_int - - lib.netplan_state_get_netdef.argtypes = [_NetplanStateP, c_char_p] - lib.netplan_state_get_netdef.restype = _NetplanNetDefinitionP - - lib.netplan_state_write_yaml_file.argtypes = [_NetplanStateP, c_char_p, c_char_p, _NetplanErrorPP] - lib.netplan_state_write_yaml_file.restype = c_int - - lib.netplan_state_update_yaml_hierarchy.argtypes = [_NetplanStateP, c_char_p, c_char_p, _NetplanErrorPP] - lib.netplan_state_update_yaml_hierarchy.restype = c_int - - lib.netplan_state_dump_yaml.argtypes = [_NetplanStateP, c_int, _NetplanErrorPP] - lib.netplan_state_dump_yaml.restype = c_int - - lib.netplan_netdef_get_embedded_switch_mode.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_get_embedded_switch_mode.restype = c_char_p - - lib.netplan_netdef_get_delay_virtual_functions_rebind.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_get_delay_virtual_functions_rebind.restype = c_int - - lib.netplan_state_get_backend.argtypes = [_NetplanStateP] - lib.netplan_state_get_backend.restype = c_int - - cls._abi_loaded = True - - def __init__(self): - self._load_abi() - self._ptr = lib.netplan_state_new() - - def __del__(self): - lib.netplan_state_clear(ctypes.byref(self._ptr)) - - def import_parser_results(self, parser): - _checked_lib_call(lib.netplan_state_import_parser_results, self._ptr, parser._ptr) - - def write_yaml_file(self, filename, rootdir): - name = filename.encode('utf-8') if filename else None - root = rootdir.encode('utf-8') if rootdir else None - _checked_lib_call(lib.netplan_state_write_yaml_file, self._ptr, name, root) - - def update_yaml_hierarchy(self, default_filename, rootdir): - name = default_filename.encode('utf-8') - root = rootdir.encode('utf-8') if rootdir else None - _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) - - def __len__(self): - return lib.netplan_state_get_netdefs_size(self._ptr) - - def __getitem__(self, def_id): - ptr = lib.netplan_state_get_netdef(self._ptr, def_id.encode('utf-8')) - if not ptr: - raise IndexError() - return NetDefinition(self, ptr) - - @property - def all_defs(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, None)) - - @property - def ethernets(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "ethernets")) - - @property - def modems(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "modems")) - - @property - def wifis(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "wifis")) - - @property - def vlans(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "vlans")) - - @property - def bridges(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "bridges")) - - @property - def bonds(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "bonds")) - - @property - def tunnels(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "tunnels")) - - @property - def vrfs(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "vrfs")) - - @property - def ovs_ports(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "_ovs-ports")) - - @property - def nm_devices(self): - return dict((nd.id, nd) for nd in _NetdefIterator(self, "nm-devices")) - - @property - def backend(self): - return lib.netplan_backend_name(lib.netplan_state_get_backend(self._ptr)).decode('utf-8') - - def dump_to_logs(self): - # Convoluted way to dump the parsed config to the logs... - with tempfile.TemporaryFile() as tmp: - self.dump_yaml(output_file=tmp) - logging.debug("Merged config:\n{}".format(tmp.read())) - - -class NetDefinition: - _abi_loaded = False - - @classmethod - def _load_abi(cls): - if cls._abi_loaded: - return - - lib.netplan_netdef_has_match.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_has_match.restype = c_int - - lib.netplan_netdef_get_id.argtypes = [_NetplanNetDefinitionP, c_char_p, c_size_t] - lib.netplan_netdef_get_id.restype = c_ssize_t - - lib.netplan_netdef_get_filepath.argtypes = [_NetplanNetDefinitionP, c_char_p, c_size_t] - lib.netplan_netdef_get_filepath.restype = c_ssize_t - - lib.netplan_netdef_get_backend.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_get_backend.restype = c_int - - lib.netplan_netdef_get_type.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_get_type.restype = c_int - - lib.netplan_netdef_get_set_name.argtypes = [_NetplanNetDefinitionP, c_char_p, c_size_t] - lib.netplan_netdef_get_set_name.restype = c_ssize_t - - lib._netplan_netdef_get_critical.argtypes = [_NetplanNetDefinitionP] - lib._netplan_netdef_get_critical.restype = c_int - - lib.netplan_netdef_get_sriov_link.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_get_sriov_link.restype = _NetplanNetDefinitionP - - lib.netplan_netdef_get_vlan_link.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_get_vlan_link.restype = _NetplanNetDefinitionP - - lib.netplan_netdef_get_bridge_link.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_get_bridge_link.restype = _NetplanNetDefinitionP - - lib.netplan_netdef_get_bond_link.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_get_bond_link.restype = _NetplanNetDefinitionP - - lib.netplan_netdef_get_peer_link.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_get_peer_link.restype = _NetplanNetDefinitionP - - lib._netplan_netdef_get_vlan_id.argtypes = [_NetplanNetDefinitionP] - lib._netplan_netdef_get_vlan_id.restype = c_uint - - lib._netplan_netdef_get_sriov_vlan_filter.argtypes = [_NetplanNetDefinitionP] - lib._netplan_netdef_get_sriov_vlan_filter.restype = c_int - - lib.netplan_netdef_match_interface.argtypes = [_NetplanNetDefinitionP] - lib.netplan_netdef_match_interface.restype = c_int - - lib.netplan_backend_name.argtypes = [c_int] - lib.netplan_backend_name.restype = c_char_p - - lib.netplan_def_type_name.argtypes = [c_int] - lib.netplan_def_type_name.restype = c_char_p - - lib._netplan_state_get_vf_count_for_def.argtypes = [_NetplanStateP, _NetplanNetDefinitionP, _NetplanErrorPP] - lib._netplan_state_get_vf_count_for_def.restype = c_int - - lib._netplan_netdef_is_trivial_compound_itf.argtypes = [_NetplanNetDefinitionP] - lib._netplan_netdef_is_trivial_compound_itf.restype = c_int - - cls._abi_loaded = True - - def __eq__(self, other): - if not hasattr(other, '_ptr'): - return False - return ctypes.addressof(self._ptr.contents) == ctypes.addressof(other._ptr.contents) - - def __init__(self, np_state, ptr): - self._load_abi() - 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 - - @property - def addresses(self): - return _NetdefAddressIterator(self._ptr) - - @property - def has_match(self): - return bool(lib.netplan_netdef_has_match(self._ptr)) - - @property - def set_name(self): - return _string_realloc_call_no_error(lambda b: lib.netplan_netdef_get_set_name(self._ptr, b, len(b))) - - @property - def critical(self): - return bool(lib._netplan_netdef_get_critical(self._ptr)) - - @property - def sriov_link(self): - link_ptr = lib.netplan_netdef_get_sriov_link(self._ptr) - if link_ptr: - return NetDefinition(self._parent, link_ptr) - return None - - @property - def vlan_link(self): - link_ptr = lib.netplan_netdef_get_vlan_link(self._ptr) - if link_ptr: - return NetDefinition(self._parent, link_ptr) - return None - - @property - def bridge_link(self): - link_ptr = lib.netplan_netdef_get_bridge_link(self._ptr) - if link_ptr: - return NetDefinition(self._parent, link_ptr) - return None - - @property - def bond_link(self): - link_ptr = lib.netplan_netdef_get_bond_link(self._ptr) - if link_ptr: - return NetDefinition(self._parent, link_ptr) - return None - - @property - def peer_link(self): - link_ptr = lib.netplan_netdef_get_peer_link(self._ptr) - if link_ptr: - return NetDefinition(self._parent, link_ptr) - return None # pragma: nocover (ovs ports are always defined in pairs) - - @property - def vlan_id(self): - vlan_id = lib._netplan_netdef_get_vlan_id(self._ptr) - # No easy way to get UINT_MAX besides this... - if vlan_id == c_uint(-1).value: - return None - return vlan_id - - @property - def has_sriov_vlan_filter(self): - return bool(lib._netplan_netdef_get_sriov_vlan_filter(self._ptr)) - - @property - def backend(self): - return lib.netplan_backend_name(lib.netplan_netdef_get_backend(self._ptr)).decode('utf-8') - - @property - def type(self): - return lib.netplan_def_type_name(lib.netplan_netdef_get_type(self._ptr)).decode('utf-8') - - @property - def id(self): - return _string_realloc_call_no_error(lambda b: lib.netplan_netdef_get_id(self._ptr, b, len(b))) - - @property - def filepath(self): - return _string_realloc_call_no_error(lambda b: lib.netplan_netdef_get_filepath(self._ptr, b, len(b))) - - @property - def embedded_switch_mode(self): - mode = lib.netplan_netdef_get_embedded_switch_mode(self._ptr) - return mode and mode.decode('utf-8') - - @property - def delay_virtual_functions_rebind(self): - return bool(lib.netplan_netdef_get_delay_virtual_functions_rebind(self._ptr)) - - def match_interface(self, itf_name=None, itf_driver=None, itf_mac=None): - return bool(lib.netplan_netdef_match_interface( - self._ptr, - itf_name and itf_name.encode('utf-8'), - itf_mac and itf_mac.encode('utf-8'), - itf_driver and itf_driver.encode('utf-8'))) - - @property - def vf_count(self): - err = ctypes.POINTER(_NetplanError)() - count = lib._netplan_state_get_vf_count_for_def(self._parent._ptr, self._ptr, ctypes.byref(err)) - if count < 0: - raise NetplanException(err.contents.message.decode('utf-8')) - return count - - @property - def is_trivial_compound_itf(self): - ''' - 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 _NetdefIterator: - _abi_loaded = False - - @classmethod - def _load_abi(cls): - if cls._abi_loaded: - return - - if not hasattr(lib, '_netplan_iter_defs_per_devtype_init'): # pragma: nocover (hard to unit-test against the WRONG lib) - raise NetplanException(''' - The current version of libnetplan does not allow iterating by devtype. - Please ensure that both the netplan CLI package and its library are up to date. - ''') - lib._netplan_state_new_netdef_pertype_iter.argtypes = [_NetplanStateP, c_char_p] - lib._netplan_state_new_netdef_pertype_iter.restype = c_void_p - - lib._netplan_iter_defs_per_devtype_next.argtypes = [c_void_p] - lib._netplan_iter_defs_per_devtype_next.restype = _NetplanNetDefinitionP - - lib._netplan_iter_defs_per_devtype_free.argtypes = [c_void_p] - lib._netplan_iter_defs_per_devtype_free.restype = None - - lib._netplan_netdef_id.argtypes = [c_void_p] - lib._netplan_netdef_id.restype = c_char_p - - cls._abi_loaded = True - - def __init__(self, np_state, devtype): - self._load_abi() - # To keep things valid, keep a reference to the parent state - self.np_state = np_state - self.iterator = lib._netplan_state_new_netdef_pertype_iter(np_state._ptr, devtype and devtype.encode('utf-8')) - - def __del__(self): - lib._netplan_iter_defs_per_devtype_free(self.iterator) - - def __iter__(self): - return self - - def __next__(self): - next_value = lib._netplan_iter_defs_per_devtype_next(self.iterator) - if not next_value: - raise StopIteration - return NetDefinition(self.np_state, next_value) - - -class _NetdefAddressIterator: - _abi_loaded = False - - @classmethod - def _load_abi(cls): - if cls._abi_loaded: - return - - if not hasattr(lib, '_netplan_new_netdef_address_iter'): # pragma: nocover (hard to unit-test against the WRONG lib) - raise NetplanException(''' - The current version of libnetplan does not allow iterating by IP addresses. - Please ensure that both the netplan CLI package and its library are up to date. - ''') - lib._netplan_new_netdef_address_iter.argtypes = [_NetplanNetDefinitionP] - lib._netplan_new_netdef_address_iter.restype = c_void_p - - lib._netplan_netdef_address_iter_next.argtypes = [c_void_p] - lib._netplan_netdef_address_iter_next.restype = _NetplanAddressP - - lib._netplan_netdef_address_free_iter.argtypes = [c_void_p] - lib._netplan_netdef_address_free_iter.restype = None - - cls._abi_loaded = True - - def __init__(self, netdef): - self._load_abi() - self.netdef = netdef - self.iterator = lib._netplan_new_netdef_address_iter(netdef) - - def __del__(self): - lib._netplan_netdef_address_free_iter(self.iterator) - - def __iter__(self): - return self - - def __next__(self): - next_value = lib._netplan_netdef_address_iter_next(self.iterator) - if not next_value: - raise StopIteration - content = next_value.contents - address = content.address.decode('utf-8') if content.address else None - lifetime = content.lifetime.decode('utf-8') if content.lifetime else None - label = content.label.decode('utf-8') if content.label else None - return NetplanAddress(address, lifetime, label) - - -lib.netplan_util_create_yaml_patch.argtypes = [c_char_p, c_char_p, c_int, _NetplanErrorPP] -lib.netplan_util_create_yaml_patch.restype = c_int - -lib.netplan_util_dump_yaml_subtree.argtypes = [c_char_p, c_int, c_int, _NetplanErrorPP] -lib.netplan_util_dump_yaml_subtree.restype = c_int - - -def create_yaml_patch(patch_object_path: List[str], patch_payload: str, patch_output): - _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()) - - -def dump_yaml_subtree(prefix, 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, 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) diff --git a/netplan_cli/meson.build b/netplan_cli/meson.build index a51578c93..16048a0f7 100644 --- a/netplan_cli/meson.build +++ b/netplan_cli/meson.build @@ -18,7 +18,6 @@ features_py = custom_target( netplan_sources = files( '__init__.py', 'configmanager.py', - 'libnetplan.py', 'terminal.py') cli_sources = files( diff --git a/python-cffi/meson.build b/python-cffi/meson.build new file mode 100644 index 000000000..74ec9066d --- /dev/null +++ b/python-cffi/meson.build @@ -0,0 +1 @@ +subdir('netplan') diff --git a/python-cffi/netplan/__init__.py b/python-cffi/netplan/__init__.py new file mode 100644 index 000000000..02c559aaa --- /dev/null +++ b/python-cffi/netplan/__init__.py @@ -0,0 +1,73 @@ +# 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] diff --git a/python-cffi/netplan/_build_cffi.py b/python-cffi/netplan/_build_cffi.py new file mode 100644 index 000000000..cb8e74a13 --- /dev/null +++ b/python-cffi/netplan/_build_cffi.py @@ -0,0 +1,142 @@ +#!/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 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 netdef_address_iter { ...; }; + + // 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); + 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_get_delay_virtual_functions_rebind(const NetplanNetDefinition* netdef); + gboolean netplan_netdef_match_interface( + const NetplanNetDefinition* netdef, const char* name, const char* mac, const char* driver_name); + + // 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_is_trivial_compound_itf(const NetplanNetDefinition* netdef); + int _netplan_state_get_vf_count_for_def( + const NetplanState* np_state, const NetplanNetDefinition* netdef, NetplanError** error); + + // 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 netdef_address_iter* _netplan_new_netdef_address_iter(NetplanNetDefinition* netdef); + NetplanAddressOptions* _netplan_netdef_address_iter_next(struct netdef_address_iter* it); + void _netplan_netdef_address_free_iter(struct netdef_address_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('.') diff --git a/python-cffi/netplan/_utils.py b/python-cffi/netplan/_utils.py new file mode 100644 index 000000000..192fbf291 --- /dev/null +++ b/python-cffi/netplan/_utils.py @@ -0,0 +1,214 @@ +# 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 diff --git a/python-cffi/netplan/meson.build b/python-cffi/netplan/meson.build new file mode 100644 index 000000000..2f820d132 --- /dev/null +++ b/python-cffi/netplan/meson.build @@ -0,0 +1,50 @@ +pymod = 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') diff --git a/python-cffi/netplan/netdef.py b/python-cffi/netplan/netdef.py new file mode 100644 index 000000000..67295c06b --- /dev/null +++ b/python-cffi/netplan/netdef.py @@ -0,0 +1,180 @@ +# 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 ._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 _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) + + # 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 _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_new_netdef_address_iter(netdef) + + def __del__(self): + lib._netplan_netdef_address_free_iter(self.iterator) + + def __iter__(self): + return self + + def __next__(self): + next_value = lib._netplan_netdef_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 'netdef_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) diff --git a/python-cffi/netplan/parser.py b/python-cffi/netplan/parser.py new file mode 100644 index 000000000..eb121320f --- /dev/null +++ b/python-cffi/netplan/parser.py @@ -0,0 +1,48 @@ +# 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')) diff --git a/python-cffi/netplan/state.py b/python-cffi/netplan/state.py new file mode 100644 index 000000000..3b0641b27 --- /dev/null +++ b/python-cffi/netplan/state.py @@ -0,0 +1,135 @@ +# 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")) diff --git a/rpm/netplan.spec b/rpm/netplan.spec index 81dce1e66..877bdf291 100644 --- a/rpm/netplan.spec +++ b/rpm/netplan.spec @@ -33,6 +33,7 @@ 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 @@ -93,6 +94,8 @@ Currently supported backends are NetworkManager and systemd-networkd. %dir %{_prefix}/lib/%{name} %{_libexecdir}/%{name}/ %{_datadir}/bash-completion/completions/%{name} +%{python3_sitelib}/%{name}/ +%{python3_sitearch}/%{name}/ # ------------------------------------------------------------------------------------------------ @@ -219,6 +222,9 @@ rm -f %{buildroot}/lib/netplan/generate rmdir %{buildroot}/lib/netplan rmdir %{buildroot}/lib +# 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} diff --git a/spread.yaml b/spread.yaml index 03c3bb737..60f154828 100644 --- a/spread.yaml +++ b/spread.yaml @@ -25,13 +25,14 @@ prepare: | apt update -qq apt install -y build-essential meson pkg-config libyaml-dev \ libglib2.0-dev uuid-dev python3 libsystemd-dev pandoc python3-pytest \ - python3-coverage libcmocka-dev python3-rich + python3-coverage libcmocka-dev python3-rich python3-cffi libpython3-dev # install, a bit ugly but this is a container (did I mention the packaging?) meson setup build --prefix=/usr meson compile -C build # FIXME: enable, this crashes right now with: # https://paste.ubuntu.com/p/qRnJvjyddN/ #meson test -C build --verbose + 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' diff --git a/src/parse-nm.c b/src/parse-nm.c index 6f4166c6c..807f73fb0 100644 --- a/src/parse-nm.c +++ b/src/parse-nm.c @@ -624,13 +624,17 @@ netplan_parser_load_keyfile(NetplanParser* npp, const char* filename, GError** e 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) { - g_warning("netplan: Keyfile: cannot find connection.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) { - g_warning("netplan: Keyfile: cannot find connection.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); @@ -908,7 +912,9 @@ netplan_parser_load_keyfile(NetplanParser* npp, const char* filename, GError** e ap = g_new0(NetplanWifiAccessPoint, 1); ap->ssid = g_key_file_get_string(kf, "wifi", "ssid", NULL); if (!ap->ssid) { - g_warning("netplan: Keyfile: cannot find SSID for WiFi connection"); + 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 diff --git a/src/parse.c b/src/parse.c index 67549a249..b930b7509 100644 --- a/src/parse.c +++ b/src/parse.c @@ -1423,6 +1423,8 @@ handle_wifi_access_points(NetplanParser* npp, yaml_node_t* node, const char* key g_debug("%s: adding wifi AP '%s'", npp->current.netdef->id, access_point->ssid); /* Check if there's already an SSID with that name */ + // FIXME: This check fails on multi-pass parsing, e.g. when defined in + // the same YAML file with a set of virtual-ethernets peers. if (npp->current.netdef->access_points && g_hash_table_lookup(npp->current.netdef->access_points, access_point->ssid)) { ret = yaml_error(npp, key, error, "%s: Duplicate access point SSID '%s'", npp->current.netdef->id, access_point->ssid); diff --git a/src/sriov.c b/src/sriov.c index d6784008c..7d4944b18 100644 --- a/src/sriov.c +++ b/src/sriov.c @@ -123,7 +123,7 @@ netplan_sriov_cleanup(const char* rootdir) } -NETPLAN_INTERNAL int +int _netplan_state_get_vf_count_for_def(const NetplanState* np_state, const NetplanNetDefinition* netdef, GError** error) { GHashTableIter iter; diff --git a/src/types-internal.h b/src/types-internal.h index 7d21d7242..7d48b6ace 100644 --- a/src/types-internal.h +++ b/src/types-internal.h @@ -99,6 +99,14 @@ typedef struct { char* label; } NetplanAddressOptions; +struct netdef_address_iter { + guint ip4_index; + guint ip6_index; + guint address_options_index; + NetplanNetDefinition* netdef; + NetplanAddressOptions* last_address; +}; + typedef struct { NetplanWifiMode mode; char* ssid; diff --git a/src/util-internal.h b/src/util-internal.h index c29abc57a..729a6d9c6 100644 --- a/src/util-internal.h +++ b/src/util-internal.h @@ -61,9 +61,6 @@ wifi_get_freq5(int channel); NETPLAN_ABI gchar* systemd_escape(char* string); -NETPLAN_INTERNAL gboolean -netplan_util_create_yaml_patch(const char* conf_obj_path, const char* obj_payload, int out_fd, GError** error); - #define OPENVSWITCH_OVS_VSCTL "/usr/bin/ovs-vsctl" void @@ -81,9 +78,6 @@ netplan_netdef_new(NetplanParser* npp, const char* id, NetplanDefType type, Netp const char * netplan_parser_get_filename(NetplanParser* npp); -NETPLAN_INTERNAL gboolean -netplan_parser_load_yaml_hierarchy(NetplanParser* npp, const char* rootdir, GError** error); - NETPLAN_INTERNAL void process_input_file(const char* f); @@ -102,6 +96,9 @@ complex_object_is_dirty(const NetplanNetDefinition* def, const void* obj, size_t 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); @@ -134,3 +131,21 @@ is_route_rule_present(const NetplanNetDefinition* netdef, const NetplanIPRule* r NETPLAN_INTERNAL gboolean //FIXME: avoid exporting private symbol is_string_in_array(GArray* array, const char* value); + +NETPLAN_INTERNAL struct netdef_address_iter* +_netplan_new_netdef_address_iter(NetplanNetDefinition* netdef); + +NETPLAN_INTERNAL NetplanAddressOptions* +_netplan_netdef_address_iter_next(struct netdef_address_iter* it); + +NETPLAN_INTERNAL void +_netplan_netdef_address_free_iter(struct netdef_address_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); diff --git a/src/util.c b/src/util.c index bbdeedad9..8944515d5 100644 --- a/src/util.c +++ b/src/util.c @@ -313,8 +313,8 @@ emit_yaml_subtree(yaml_parser_t *parser, yaml_emitter_t *emitter, char** yaml_pa return FALSE; } -NETPLAN_INTERNAL gboolean -netplan_util_dump_yaml_subtree(const char* prefix, int input_fd, int output_fd, GError** error) { +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; @@ -715,15 +715,7 @@ get_unspecified_address(int ip_family) return (ip_family == AF_INET) ? "0.0.0.0" : "::"; } -struct netdef_address_iter { - guint ip4_index; - guint ip6_index; - guint address_options_index; - NetplanNetDefinition* netdef; - NetplanAddressOptions* last_address; -}; - -NETPLAN_INTERNAL struct netdef_address_iter* +struct netdef_address_iter* _netplan_new_netdef_address_iter(NetplanNetDefinition* netdef) { struct netdef_address_iter* it = g_malloc0(sizeof(struct netdef_address_iter)); @@ -747,7 +739,7 @@ _netplan_new_netdef_address_iter(NetplanNetDefinition* netdef) * 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. */ -NETPLAN_INTERNAL NetplanAddressOptions* +NetplanAddressOptions* _netplan_netdef_address_iter_next(struct netdef_address_iter* it) { NetplanAddressOptions* options = NULL; @@ -784,7 +776,7 @@ _netplan_netdef_address_iter_next(struct netdef_address_iter* it) return options; } -NETPLAN_INTERNAL void +void _netplan_netdef_address_free_iter(struct netdef_address_iter* it) { if (it->last_address) @@ -798,7 +790,7 @@ struct netdef_pertype_iter { NetplanState* np_state; }; -NETPLAN_INTERNAL struct netdef_pertype_iter* +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; @@ -811,7 +803,7 @@ _netplan_state_new_netdef_pertype_iter(NetplanState* np_state, const char* def_t } -NETPLAN_INTERNAL NetplanNetDefinition* +NetplanNetDefinition* _netplan_netdef_pertype_iter_next(struct netdef_pertype_iter* it) { gpointer key, value; @@ -827,7 +819,7 @@ _netplan_netdef_pertype_iter_next(struct netdef_pertype_iter* it) return NULL; } -NETPLAN_INTERNAL void +void _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it) { g_free(it); diff --git a/tests/cli/test_get_set.py b/tests/cli/test_get_set.py index 4deebc141..9e60fc352 100644 --- a/tests/cli/test_get_set.py +++ b/tests/cli/test_get_set.py @@ -26,7 +26,7 @@ import yaml from netplan_cli.cli.commands.set import FALLBACK_FILENAME -from netplan_cli.libnetplan import NetplanException +from netplan import NetplanException from tests.test_utils import call_cli diff --git a/tests/ctests/meson.build b/tests/ctests/meson.build index 15b7390a9..8a41e9a30 100644 --- a/tests/ctests/meson.build +++ b/tests/ctests/meson.build @@ -12,11 +12,10 @@ tests = { cmocka = dependency('cmocka', required: true) -inc_tests = include_directories('../../src') foreach name, should_fail: tests exe = executable(name, '@0@.c'.format(name), - include_directories: [inc, inc_tests], + include_directories: [inc, inc_internal], dependencies: [cmocka, glib, gio, yaml, uuid], c_args: [ '-DFIXTURESDIR="' + meson.project_source_root() + '/tests/ctests/fixtures"', diff --git a/tests/parser/base.py b/tests/parser/base.py index 0ef924146..9b1f08081 100644 --- a/tests/parser/base.py +++ b/tests/parser/base.py @@ -19,15 +19,13 @@ # along with this program. If not, see . from configparser import ConfigParser -from netplan_cli.libnetplan import _NetplanError +import netplan import os import re import sys import shutil import tempfile import unittest -import ctypes -import ctypes.util import contextlib import subprocess @@ -41,8 +39,6 @@ # make sure we fail on criticals os.environ['G_DEBUG'] = 'fatal-criticals' -lib = ctypes.CDLL(ctypes.util.find_library('netplan')) - WOKE_REPLACE_REGEX = ' +# wokeignore:rule=[a-z]+' @@ -72,29 +68,30 @@ def setUp(self): os.makedirs(self.confdir) def tearDown(self): - lib.netplan_clear_netdefs() 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''' - err = ctypes.POINTER(_NetplanError)() # 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: - found_values = 0 - uuid = 'UNKNOWN_UUID' - 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 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, @@ -105,21 +102,25 @@ def generate_from_keyfile(self, keyfile, netdef_id=None, expect_fail=False, file file.write(keyfile) with capture_stderr() as outf: + parser = netplan.Parser() if expect_fail: - self.assertFalse(lib.netplan_parse_keyfile(f.encode(), ctypes.byref(err))) - if err: - return err.contents.message.decode('utf-8') + try: + parser.load_keyfile(f) + except netplan.NetplanException as err: + return err.message else: - self.assertTrue(lib.netplan_parse_keyfile(f.encode(), ctypes.byref(err))) - if err: # pragma: nocover (only happens if a test fails so irrelevant for coverage) - return err.contents.message.decode('utf-8') + 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) - lib._write_netplan_conf(netdef_id.encode(), self.workdir.name.encode()) - lib.netplan_clear_netdefs() + 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}) diff --git a/tests/test_configmanager.py b/tests/test_configmanager.py index f03889282..e3b526099 100644 --- a/tests/test_configmanager.py +++ b/tests/test_configmanager.py @@ -169,6 +169,20 @@ def setUp(self): 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) @@ -183,7 +197,7 @@ def test_parse(self): self.assertIn('eth0', state.ethernets) self.assertIn('bond6', state.bonds) self.assertIn('eth0', self.configmanager.physical_interfaces) - self.assertNotIn('bond7', self.configmanager.all_defs) + 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) @@ -201,6 +215,9 @@ def test_parse(self): 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) diff --git a/tests/test_libnetplan.py b/tests/test_libnetplan.py index 00299465d..36681b4dc 100644 --- a/tests/test_libnetplan.py +++ b/tests/test_libnetplan.py @@ -17,6 +17,7 @@ # 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 @@ -30,9 +31,14 @@ from utils import state_from_yaml from netplan_cli.cli.commands.set import FALLBACK_FILENAME -import netplan_cli.libnetplan as libnetplan +import netplan + +# 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(ctypes.util.find_library('netplan')) +# Define some libnetplan.so ABI +lib.netplan_get_id_from_nm_filename.restype = ctypes.c_char_p -lib = libnetplan.lib rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) exe_cli = os.path.join(rootdir, 'src', 'netplan.script') @@ -169,8 +175,8 @@ def test_write_netplan_conf(self): class TestNetdefIterator(TestBase): def test_with_empty_netplan(self): - state = libnetplan.State() - self.assertSequenceEqual(list(libnetplan._NetdefIterator(state, "ethernets")), []) + state = netplan.State() + self.assertSequenceEqual(list(netplan.netdef.NetDefinitionIterator(state, "ethernets")), []) def test_iter_all_types(self): state = state_from_yaml(self.confdir, '''network: @@ -180,7 +186,7 @@ def test_iter_all_types(self): bridges: br0: dhcp4: false''') - self.assertSetEqual(set(["eth0", "br0"]), set(d.id for d in libnetplan._NetdefIterator(state, None))) + self.assertSetEqual(set(["eth0", "br0"]), set(d.id for d in netplan.netdef.NetDefinitionIterator(state, None))) def test_iter_ethernets(self): state = state_from_yaml(self.confdir, '''network: @@ -192,7 +198,7 @@ def test_iter_ethernets(self): bridges: br0: dhcp4: false''') - self.assertSetEqual(set(["eth0", "eth1"]), set(d.id for d in libnetplan._NetdefIterator(state, "ethernets"))) + self.assertSetEqual(set(["eth0", "eth1"]), set(d.id for d in netplan.netdef.NetDefinitionIterator(state, "ethernets"))) class TestNetdefAddressesIterator(TestBase): @@ -202,7 +208,7 @@ def test_with_empty_ip_addresses(self): eth0: dhcp4: true''') - netdef = next(libnetplan._NetdefIterator(state, "ethernets")) + netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) self.assertSetEqual(set(), set(ip for ip in netdef.addresses)) def test_iter_ethernets(self): @@ -216,7 +222,7 @@ def test_iter_ethernets(self): - 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(libnetplan._NetdefIterator(state, "ethernets")) + 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)) @@ -236,7 +242,7 @@ def test_iter_ethernets_with_options(self): 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(libnetplan._NetdefIterator(state, "ethernets")) + 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)) @@ -249,7 +255,7 @@ def test_drop_iterator_before_finishing(self): - 192.168.0.1/24 - 1234:4321:abcd::cdef/96''') - netdef = next(libnetplan._NetdefIterator(state, "ethernets")) + netdef = next(netplan.netdef.NetDefinitionIterator(state, "ethernets")) iter = netdef.addresses.__iter__() address = next(iter) self.assertEqual(address.address, "192.168.0.1/24") @@ -258,23 +264,23 @@ def test_drop_iterator_before_finishing(self): class TestParser(TestBase): def test_load_yaml_from_fd_empty(self): - parser = libnetplan.Parser() + 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 = libnetplan.Parser() + parser = netplan.Parser() with tempfile.TemporaryFile() as f: f.write(b'invalid: {]') f.seek(0, io.SEEK_SET) - with self.assertRaises(libnetplan.NetplanParserException) as context: + with self.assertRaises(netplan.NetplanParserException) as context: parser.load_yaml(f) self.assertIn('Invalid YAML', str(context.exception)) def test_load_keyfile(self): - parser = libnetplan.Parser() - state = libnetplan.State() + parser = netplan.Parser() + state = netplan.State() with tempfile.NamedTemporaryFile() as f: f.write(b'''[connection] id=Bridge connection 1 @@ -292,7 +298,7 @@ def test_load_keyfile(self): parser.load_keyfile(f.name) state.import_parser_results(parser) output = io.StringIO() - state.dump_yaml(output) + state._dump_yaml(output) yaml_data = yaml.safe_load(output.getvalue()) self.assertIsNotNone(yaml_data.get('network')) @@ -307,7 +313,7 @@ def test_get_netdef(self): self.assertEqual("eth0", netdef.id) def test_get_netdef_empty_state(self): - state = libnetplan.State() + state = netplan.State() with self.assertRaises(IndexError): state['eth0'] @@ -327,8 +333,8 @@ def test_get_netdefs_size(self): self.assertEqual(1, len(state)) def test_bad_state(self): - state = libnetplan.State() - parser = libnetplan.Parser() + state = netplan.State() + parser = netplan.Parser() with tempfile.NamedTemporaryFile() as f: f.write(b'''network: renderer: networkd @@ -343,7 +349,7 @@ def test_bad_state(self): f.flush() parser.load_yaml(f.name) - with self.assertRaises(libnetplan.NetplanException): + with self.assertRaises(netplan.NetplanException): state.import_parser_results(parser) def test_dump_yaml_bad_file_perms(self): @@ -354,15 +360,16 @@ def test_dump_yaml_bad_file_perms(self): bad_file = os.path.join(self.workdir.name, 'bad.yml') open(bad_file, 'a').close() os.chmod(bad_file, 0o444) - with self.assertRaises(libnetplan.NetplanFileException) as context: + with self.assertRaises(netplan.NetplanFileException) as context: with open(bad_file) as f: - state.dump_yaml(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 = libnetplan.State() + state = netplan.State() with tempfile.TemporaryFile() as f: - state.dump_yaml(f) + state._dump_yaml(f) f.flush() self.assertEqual(0, f.seek(0, io.SEEK_END)) @@ -375,8 +382,8 @@ def test_write_yaml_file_unremovable_target(self): os.remove(target) os.makedirs(target) - with self.assertRaises(libnetplan.NetplanFileException): - state.write_yaml_file('target.yml', self.workdir.name) + 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: @@ -384,17 +391,17 @@ def test_update_yaml_hierarchy_no_confdir(self): eth0: dhcp4: false''') shutil.rmtree(self.confdir) - with self.assertRaises(libnetplan.NetplanFileException) as context: - state.update_yaml_hierarchy("bogus", self.workdir.name) + 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 = libnetplan.State() + state = netplan.State() os.makedirs(self.confdir) with tempfile.TemporaryDirectory(dir=self.confdir) as tmpdir: hint = os.path.basename(tmpdir) - with self.assertRaises(libnetplan.NetplanFileException): - state.write_yaml_file(hint, self.workdir.name) + 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: @@ -402,8 +409,8 @@ def test_write_yaml_file_file_no_confdir(self): eth0: dhcp4: false''', filename='test.yml') shutil.rmtree(self.confdir) - with self.assertRaises(libnetplan.NetplanFileException) as context: - state.write_yaml_file('test.yml', self.workdir.name) + 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)) @@ -445,7 +452,7 @@ def test_eq(self): eth1: dhcp4: false''') - # libnetplan.State __getitem__ doesn't cache the netdefs, + # 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']) @@ -481,8 +488,8 @@ def test_filepath_for_ovs_ports(self): self.assertEqual(os.path.join(self.confdir, "a.yaml"), netdef_port2.filepath) def test_filepath_for_ovs_ports_when_conf_is_redefined(self): - state = libnetplan.State() - parser = libnetplan.Parser() + state = netplan.State() + parser = netplan.Parser() with tempfile.NamedTemporaryFile() as f: f.write(b'''network: @@ -547,15 +554,15 @@ def test_simple_matches(self): 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(itf_name="eth42")) - self.assertFalse(state['name-match'].match_interface(itf_name="eth32")) - self.assertTrue(state['driver-match'].match_interface(itf_driver="e1000")) - self.assertFalse(state['name-match'].match_interface(itf_driver="ixgbe")) - self.assertFalse(state['driver-match'].match_interface(itf_name="eth42")) - self.assertTrue(state['mac-match'].match_interface(itf_mac="11:22:33:AA:BB:FF")) - self.assertFalse(state['mac-match'].match_interface(itf_mac="11:22:33:AA:BB:CC")) + 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: @@ -564,8 +571,8 @@ def test_match_without_match_block(self): dhcp4: false''') netdef = state['eth0'] - self.assertTrue(netdef.match_interface('eth0')) - self.assertFalse(netdef.match_interface('eth000')) + 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: @@ -573,8 +580,8 @@ def test_vlan_props_without_vlan(self): eth0: dhcp4: false''') - self.assertIsNone(state['eth0'].vlan_id) - self.assertIsNone(state['eth0'].vlan_link) + 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: @@ -589,9 +596,9 @@ def test_is_trivial_compound_itf(self): 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) + 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: @@ -605,7 +612,7 @@ def test_interface_has_pointer_to_bridge(self): - eth0 ''') - self.assertEqual(state['eth0'].bridge_link.id, "br0") + 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: @@ -614,7 +621,7 @@ def test_interface_pointer_to_bridge_is_none(self): dhcp4: false ''') - self.assertIsNone(state['eth0'].bridge_link) + self.assertIsNone(state['eth0'].links.get('bridge')) def test_interface_has_pointer_to_bond(self): state = state_from_yaml(self.confdir, '''network: @@ -628,7 +635,7 @@ def test_interface_has_pointer_to_bond(self): - eth0 ''') - self.assertEqual(state['eth0'].bond_link.id, "bond0") + 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: @@ -637,7 +644,7 @@ def test_interface_pointer_to_bond_is_none(self): dhcp4: false ''') - self.assertIsNone(state['eth0'].bond_link) + self.assertIsNone(state['eth0'].links.get('bond')) def test_interface_has_pointer_to_peer(self): state = state_from_yaml(self.confdir, '''network: @@ -653,15 +660,24 @@ def test_interface_has_pointer_to_peer(self): interfaces: [patch0-1, bond0] ''') - self.assertEqual(state['patch0-1'].peer_link.id, "patch1-0") - self.assertEqual(state['patch1-0'].peer_link.id, "patch0-1") + self.assertEqual(state['patch0-1'].links.get('peer').id, "patch1-0") + self.assertEqual(state['patch1-0'].links.get('peer').id, "patch0-1") 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(libnetplan.NetplanFormatException) as context: - libnetplan.create_yaml_patch(['network'], '{invalid_yaml]', 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) @@ -669,8 +685,8 @@ def test_create_yaml_patch_bad_syntax(self): 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(libnetplan.NetplanFileException) as context: - libnetplan.dump_yaml_subtree('network', f, 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): @@ -681,8 +697,8 @@ def test_dump_yaml_subtree_bad_output_file_perms(self): output.write('') with open(input_file, "r") as f, open(output_file, 'r') as output: - with self.assertRaises(libnetplan.NetplanFileException) as context: - libnetplan.dump_yaml_subtree('network', f, 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): @@ -690,8 +706,8 @@ def test_dump_yaml_subtree_bad_yaml_outside(self): with open(input_file, "w+") as f, tempfile.TemporaryFile() as output: f.write('{garbage)') f.flush() - with self.assertRaises(libnetplan.NetplanFormatException) as context: - libnetplan.dump_yaml_subtree('network', f, output) + 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): @@ -702,8 +718,8 @@ def test_dump_yaml_subtree_bad_yaml_inside(self): {garbage)''') f.flush() - with self.assertRaises(libnetplan.NetplanFormatException) as context: - libnetplan.dump_yaml_subtree('network', f, output) + 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): @@ -712,8 +728,8 @@ def test_dump_yaml_subtree_bad_type(self): f.write('''[]''') f.flush() - with self.assertRaises(libnetplan.NetplanFormatException) as context: - libnetplan.dump_yaml_subtree('network', f, output) + 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): @@ -724,8 +740,8 @@ def test_dump_yaml_subtree_bad_yaml_ignored(self): ignored: - [}''') f.flush() - with self.assertRaises(libnetplan.NetplanFormatException) as context: - libnetplan.dump_yaml_subtree('network', f, output) + 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): @@ -736,7 +752,7 @@ def test_dump_yaml_subtree_discard_tail(self): tail: - []''') f.flush() - libnetplan.dump_yaml_subtree('network\tethernets', f, output) + netplan._dump_yaml_subtree(['network', 'ethernets'], f, output) output.seek(0) self.assertEqual(yaml.safe_load(output), {}) @@ -748,13 +764,13 @@ def test_dump_yaml_absent_key(self): tail: - []''') f.flush() - libnetplan.dump_yaml_subtree('network\tethernets\teth0', f, output) + 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 = libnetplan.Parser() + parser = netplan.Parser() with tempfile.TemporaryDirectory() as d: full_dir = d + '/etc/netplan' os.makedirs(full_dir) @@ -765,7 +781,7 @@ def test_validation_error_exception(self): set-name: abc''') f.flush() - with self.assertRaises(libnetplan.NetplanValidationException): + with self.assertRaises(netplan.NetplanValidationException): parser.load_yaml_hierarchy(d) def test_validation_exception_with_bad_error_message(self): @@ -775,7 +791,7 @@ def test_validation_exception_with_bad_error_message(self): This situation should never happen though. ''' with self.assertRaises(ValueError): - libnetplan.NetplanValidationException('not the expected file path', 0, 0) + netplan.NetplanValidationException('not the expected file path', 0, 0) def test_parser_exception_with_bad_error_message(self): ''' @@ -784,4 +800,4 @@ def test_parser_exception_with_bad_error_message(self): This situation should never happen though. ''' with self.assertRaises(ValueError): - libnetplan.NetplanParserException('not the expected file path, line and column', 0, 0) + netplan.NetplanParserException('not the expected file path, line and column', 0, 0) diff --git a/tests/test_ovs.py b/tests/test_ovs.py index a0734aa3e..5b44a0289 100644 --- a/tests/test_ovs.py +++ b/tests/test_ovs.py @@ -126,7 +126,7 @@ def test_is_ovs_interface(self): ethernets: ovs0: openvswitch: {}''') - self.assertTrue(ovs.is_ovs_interface('ovs0', state.all_defs)) + self.assertTrue(ovs.is_ovs_interface('ovs0', state.netdefs)) def test_is_ovs_interface_false(self): with tempfile.TemporaryDirectory() as root: @@ -139,7 +139,7 @@ def test_is_ovs_interface_false(self): interfaces: - eth0 - eth1''') - self.assertFalse(ovs.is_ovs_interface('br0', state.all_defs)) + self.assertFalse(ovs.is_ovs_interface('br0', state.netdefs)) def test_is_ovs_interface_recursive(self): with tempfile.TemporaryDirectory() as root: @@ -153,4 +153,4 @@ def test_is_ovs_interface_recursive(self): bonds: bond0: interfaces: [patch1-0, eth0]''') - self.assertTrue(ovs.is_ovs_interface('bond0', state.all_defs)) + self.assertTrue(ovs.is_ovs_interface('bond0', state.netdefs)) diff --git a/tests/test_sriov.py b/tests/test_sriov.py index 2287895c2..119489dfe 100644 --- a/tests/test_sriov.py +++ b/tests/test_sriov.py @@ -24,8 +24,8 @@ from collections import defaultdict from unittest.mock import patch, mock_open, call +import netplan import netplan_cli.cli.sriov as sriov -import netplan_cli.libnetplan as libnetplan from netplan_cli.configmanager import ConfigManager, ConfigurationError from generator.base import TestBase @@ -578,7 +578,7 @@ def test_apply_sriov_config_invalid_vlan(self, gim, gidn, apply_vlan, quirks, gim.return_value = '00:01:02:03:04:05' # call method under test - with self.assertRaises(libnetplan.NetplanValidationException) as e: + with self.assertRaises(netplan.NetplanValidationException) as e: sriov.apply_sriov_config(self.configmanager, rootdir=self.workdir.name) self.assertIn('vf1.15: missing \'id\' property', str(e.exception)) diff --git a/tests/test_utils.py b/tests/test_utils.py index 349427f14..ec57087e6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -22,11 +22,11 @@ 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 -import netplan_cli.libnetplan as libnetplan from unittest.mock import patch @@ -125,9 +125,9 @@ def setUp(self): def load_conf(self, conf_txt): with open(self.default_conf, 'w') as f: f.write(conf_txt) - parser = libnetplan.Parser() + parser = netplan.Parser() parser.load_yaml_hierarchy(rootdir=self.workdir.name) - state = libnetplan.State() + state = netplan.State() state.import_parser_results(parser) return state diff --git a/tests/utils.py b/tests/utils.py index d056e91f5..95539ad5f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,5 @@ import os -import netplan_cli.libnetplan as libnetplan +import netplan def state_from_yaml(confdir, yaml, filename="a.yml"): @@ -7,8 +7,8 @@ def state_from_yaml(confdir, yaml, filename="a.yml"): conf = os.path.join(confdir, filename) with open(conf, "w+") as f: f.write(yaml) - parser = libnetplan.Parser() + parser = netplan.Parser() parser.load_yaml(conf) - state = libnetplan.State() + state = netplan.State() state.import_parser_results(parser) return state diff --git a/tools/keyfile_to_yaml.py b/tools/keyfile_to_yaml.py index 4f8bf3331..4dc4971c8 100644 --- a/tools/keyfile_to_yaml.py +++ b/tools/keyfile_to_yaml.py @@ -6,14 +6,14 @@ import io import sys -from netplan_cli import libnetplan +import netplan if len(sys.argv) < 2: print("Pass the NM keyfile as parameter") sys.exit(1) -parser = libnetplan.Parser() -state = libnetplan.State() +parser = netplan.Parser() +state = netplan.State() try: parser.load_keyfile(sys.argv[1]) @@ -23,6 +23,6 @@ sys.exit(1) output = io.StringIO() -state.dump_yaml(output) +state._dump_yaml(output) print(output.getvalue())