From 754d4f0416cd0d400ea55d0a45f9fdf500ebc408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Tue, 1 Aug 2023 09:25:32 +0200 Subject: [PATCH] WIP: netplan get --- Makefile | 1 + include/parse.h | 5 +- include/util.h | 3 + meson.build | 2 +- netplan_cli/cli/core.py | 2 +- netplan_cli/cli/state.py | 9 +- netplan_cli/libnetplan.py | 165 +------------------- python-cffi/__init__.py | 18 --- python-cffi/meson.build | 42 +---- python-cffi/netplan/__init__.py | 58 +++++++ python-cffi/{ => netplan}/_build_cffi.py | 29 +++- python-cffi/netplan/_utils.py | 190 +++++++++++++++++++++++ python-cffi/netplan/meson.build | 55 +++++++ python-cffi/netplan/parser.py | 47 ++++++ python-cffi/netplan/state.py | 47 ++++++ python-cffi/parser.py | 24 --- python-cffi/state.py | 21 --- src/util-internal.h | 3 - src/util.c | 4 +- 19 files changed, 444 insertions(+), 281 deletions(-) delete mode 100644 python-cffi/__init__.py create mode 100644 python-cffi/netplan/__init__.py rename python-cffi/{ => netplan}/_build_cffi.py (73%) create mode 100644 python-cffi/netplan/_utils.py create mode 100644 python-cffi/netplan/meson.build create mode 100644 python-cffi/netplan/parser.py create mode 100644 python-cffi/netplan/state.py delete mode 100644 python-cffi/parser.py delete mode 100644 python-cffi/state.py diff --git a/Makefile b/Makefile index 6426ce753..6cf1266cb 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/_libnetplan0.* check: default meson test -C _build --verbose 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/util.h b/include/util.h index c495ce1a7..fef7b35e0 100644 --- a/include/util.h +++ b/include/util.h @@ -54,6 +54,9 @@ netplan_state_iterator_next(NetplanStateIterator* iter); NETPLAN_PUBLIC gboolean netplan_state_iterator_has_next(const NetplanStateIterator* iter); +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 8a59cfe84..b99225dc6 100644 --- a/meson.build +++ b/meson.build @@ -61,7 +61,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'), 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/state.py b/netplan_cli/cli/state.py index 500eaca2c..408638455 100644 --- a/netplan_cli/cli/state.py +++ b/netplan_cli/cli/state.py @@ -32,7 +32,8 @@ import dbus from . import utils -from .. import libnetplan + +from netplan import Parser, State, _dump_yaml_subtree JSON = Union[Dict[str, 'JSON'], List['JSON'], int, str, float, bool, Type[None]] @@ -485,10 +486,10 @@ class NetplanConfigState(): def __init__(self, subtree='all', rootdir='/'): - parser = libnetplan.Parser() + parser = Parser() parser.load_yaml_hierarchy(rootdir) - np_state = libnetplan.State() + np_state = State() np_state.import_parser_results(parser) self.state = StringIO() @@ -503,7 +504,7 @@ def __init__(self, subtree='all', rootdir='/'): tmp_in = StringIO() np_state.dump_yaml(output_file=tmp_in) - libnetplan.dump_yaml_subtree(subtree, tmp_in, self.state) + _dump_yaml_subtree(subtree, tmp_in, self.state) def __str__(self) -> str: return self.state.getvalue() diff --git a/netplan_cli/libnetplan.py b/netplan_cli/libnetplan.py index 13b5205e9..3d4ac9c9f 100644 --- a/netplan_cli/libnetplan.py +++ b/netplan_cli/libnetplan.py @@ -18,173 +18,22 @@ 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 +from netplan import (NetplanException, NetplanParserException, + NetplanValidationException, NetplanFileException, + NetplanFormatException) +from netplan._utils import NETPLAN_EXCEPTIONS -# 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, - } - }) +# Re-export +__all__ = [NetplanParserException, NetplanValidationException, + NetplanFileException, NetplanFormatException] class _NetplanError(ctypes.Structure): diff --git a/python-cffi/__init__.py b/python-cffi/__init__.py deleted file mode 100644 index 54a2ca587..000000000 --- a/python-cffi/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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 . - -# Re-export submodules -from .parser import * -from .state import * \ No newline at end of file diff --git a/python-cffi/meson.build b/python-cffi/meson.build index 83b30800c..74ec9066d 100644 --- a/python-cffi/meson.build +++ b/python-cffi/meson.build @@ -1,41 +1 @@ -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: '_libnetplan0.c', -) - -# See https://github.com/grimme-lab/xtb-python/blob/main/meson.build -cffi_pyext = python.extension_module( - '_libnetplan0', - link_whole: static_library( - '_libnetplan0', - cffi_srcs, - dependencies: [python_dep, glib], - include_directories: [inc], - ), - dependencies: [python_dep], - include_directories: [include_directories(join_paths('..', 'include'))], - subdir: 'netplan', - install: true, -) - -bindings_sources = ''' - __init__.py - parser.py - state.py -'''.split() - -bindings = python.install_sources( - [bindings_sources], - subdir: 'netplan') +subdir('netplan') diff --git a/python-cffi/netplan/__init__.py b/python-cffi/netplan/__init__.py new file mode 100644 index 000000000..9f19194f9 --- /dev/null +++ b/python-cffi/netplan/__init__.py @@ -0,0 +1,58 @@ +# 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 os +from typing import IO + +from ._libnetplan0 import lib +from .parser import Parser +from .state import State +from ._utils import (_checked_lib_call, NetplanException, + NetplanValidationException, NetplanParserException, + NetplanFileException, NetplanFormatException) + + +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) + + +# Re-export submodules +__all__ = [Parser, State, NetplanException, NetplanValidationException, + NetplanParserException, NetplanFileException, NetplanFormatException, + _dump_yaml_subtree] diff --git a/python-cffi/_build_cffi.py b/python-cffi/netplan/_build_cffi.py similarity index 73% rename from python-cffi/_build_cffi.py rename to python-cffi/netplan/_build_cffi.py index bcea315dd..00cb76d3b 100644 --- a/python-cffi/_build_cffi.py +++ b/python-cffi/netplan/_build_cffi.py @@ -24,6 +24,14 @@ # 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(""" + //FIXME: avoid private struct definition + struct GError + { + unsigned int domain; + int code; + char *message; + }; + typedef int gint; typedef gint gboolean; typedef struct GError NetplanError; @@ -41,15 +49,20 @@ 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, GError** 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, GError** error); + gboolean netplan_parser_load_nullable_overrides( + NetplanParser* npp, int input_fd, const char* constraint, NetplanError** error); NetplanState* netplan_state_new(); void netplan_state_reset(NetplanState* np_state); 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_dump_yaml(const NetplanState* np_state, int output_fd, NetplanError** error); + gboolean netplan_util_dump_yaml_subtree(const char* prefix, int input_fd, int output_fd, NetplanError** error); """) cffi_inc = os.getenv('CFFI_INC', sys.argv[1]) @@ -59,17 +72,19 @@ # 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("_libnetplan0", ['glib-2.0'], -""" +ffibuilder.set_source_pkgconfig( + "_libnetplan0", ['glib-2.0'], + """ // the C header of the library #include "netplan.h" #include "parse.h" #include "parse-nm.h" -""", + #include "util.h" + """, include_dirs=[cffi_inc], library_dirs=[cffi_lib], - libraries=['netplan', 'glib-2.0']) # library name, for the linker + libraries=['glib-2.0']) # library name, for the linker if __name__ == "__main__": - #ffibuilder.compile(verbose=False) + # ffibuilder.compile(verbose=False) ffibuilder.distutils_extension('.') diff --git a/python-cffi/netplan/_utils.py b/python-cffi/netplan/_utils.py new file mode 100644 index 000000000..7145694bb --- /dev/null +++ b/python-cffi/netplan/_utils.py @@ -0,0 +1,190 @@ +# 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 ._libnetplan0 import ffi + + +# 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, + } + }) + + +def _checked_lib_call(fn, *args): + ref = ffi.new('NetplanError **') + ret = bool(fn(*args, ref)) + err = ref[0] + if not ret: + error_domain = err.domain + error_code = err.code + error_message = ffi.string(err.message).decode('utf-8') + exception = NETPLAN_EXCEPTIONS[error_domain][error_code] + raise exception(error_message, error_domain, error_code) diff --git a/python-cffi/netplan/meson.build b/python-cffi/netplan/meson.build new file mode 100644 index 000000000..41956d2e7 --- /dev/null +++ b/python-cffi/netplan/meson.build @@ -0,0 +1,55 @@ +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: '_libnetplan0.c', +) + +# See https://github.com/grimme-lab/xtb-python/blob/main/meson.build +cffi_pyext = python.extension_module( + '_libnetplan0', + link_whole: static_library( + '_libnetplan0', + cffi_srcs, + dependencies: [python_dep, glib], + include_directories: [inc], + link_with: [libnetplan], + ), + dependencies: [python_dep], + link_with: [libnetplan], + include_directories: [inc], + subdir: 'netplan', + install: true, +) + +bindings_sources = ''' + __init__.py + parser.py + state.py + _utils.py +'''.split() + +# 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: true, + depends: cffi_pyext) +endforeach + +bindings = python.install_sources( + [bindings_sources], + subdir: 'netplan') diff --git a/python-cffi/netplan/parser.py b/python-cffi/netplan/parser.py new file mode 100644 index 000000000..e63c80b89 --- /dev/null +++ b/python-cffi/netplan/parser.py @@ -0,0 +1,47 @@ +# 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 ._libnetplan0 import ffi, lib # FIXME: rename binary module to _netplan_cffi +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): + _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: str): + _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')) diff --git a/python-cffi/netplan/state.py b/python-cffi/netplan/state.py new file mode 100644 index 000000000..e987004c5 --- /dev/null +++ b/python-cffi/netplan/state.py @@ -0,0 +1,47 @@ +# 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 + +from io import StringIO +from typing import IO + +from ._libnetplan0 import ffi, lib +from ._utils import _checked_lib_call + + +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 import_parser_results(self, parser): + _checked_lib_call(lib.netplan_state_import_parser_results, self._ptr, parser._ptr) + + 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) diff --git a/python-cffi/parser.py b/python-cffi/parser.py deleted file mode 100644 index 538766831..000000000 --- a/python-cffi/parser.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 ._libnetplan0 import ffi, lib - -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) diff --git a/python-cffi/state.py b/python-cffi/state.py deleted file mode 100644 index 908d79284..000000000 --- a/python-cffi/state.py +++ /dev/null @@ -1,21 +0,0 @@ -# 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 ._libnetplan0 import ffi, lib - -class State(): - def __init__(self): - state = lib.netplan_state_new() - print(lib.netplan_state_get_backend(state)) diff --git a/src/util-internal.h b/src/util-internal.h index 4b81e0b2d..7034fcfd2 100644 --- a/src/util-internal.h +++ b/src/util-internal.h @@ -81,9 +81,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); diff --git a/src/util.c b/src/util.c index 92e09a6e8..421b2ceae 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;