Skip to content

Commit

Permalink
WIP: netplan get
Browse files Browse the repository at this point in the history
  • Loading branch information
slyon committed Aug 2, 2023
1 parent a950570 commit 754d4f0
Show file tree
Hide file tree
Showing 19 changed files with 444 additions and 281 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion include/parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -51,7 +54,7 @@ netplan_state_import_parser_results(NetplanState* np_state, NetplanParser* npp,
* <constraint> 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 ***********/

Expand Down
3 changes: 3 additions & 0 deletions include/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
2 changes: 1 addition & 1 deletion netplan_cli/cli/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
9 changes: 5 additions & 4 deletions netplan_cli/cli/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]

Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down
165 changes: 7 additions & 158 deletions netplan_cli/libnetplan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<file_path>.*\.yaml): (?P<message>.*)'
)

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<file_path>.*):(?P<error_line>\d+):(?P<error_col>\d+): (?P<message>(\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):
Expand Down
18 changes: 0 additions & 18 deletions python-cffi/__init__.py

This file was deleted.

42 changes: 1 addition & 41 deletions python-cffi/meson.build
Original file line number Diff line number Diff line change
@@ -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')
58 changes: 58 additions & 0 deletions python-cffi/netplan/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright (C) 2023 Canonical, Ltd.
# Author: Lukas Märdian <[email protected]>
#
# 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 <http://www.gnu.org/licenses/>.

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]
Loading

0 comments on commit 754d4f0

Please sign in to comment.