[spec] API and Bindings definition for libnetplan

Internal ID: FO037
Authors: Simon Chopin, Lukas Märdian

Abstract

Libnetplan is a component of the netplan.io project that contains the logic for data parsing, validation and generation. It is build as a dynamic .so library that can be used from different binaries (like netplan’s “generate”, “netplan-dbus”, the “netplan apply/try/get/set/…” CLI via the corresponding Python bindings or external applications like the NetworkManager, using the netplan backend). We want to have properly defined API and Python bindings for this that is consistent and future proof.

Rationale

Libnetplan was introduced by ripping out a bunch of code from the netplan’s “generate” binary, which lead to interfaces that are not really thought through, with inconsistent naming of API functions and exporting of unnecessary internal symbols.

We want to fix that and design the API in a future-proof way.

Specification

General convention

The general rule for our API is to export as few public symbols as necessary, following the convention of calling them “netplan_OBJECT_ACTION” using a “_” prefix for symbols that are supposed to be used internally only (e.g. by netplan CLI). The ACTION should resemble the stanza name used in the YAML structure (if any) as closely as possible. Some examples:

  • netplan_state_write_yaml()
  • netplan_state_clear()
  • netplan_parser_new()
  • netplan_parser_load_yaml()
  • netplan_parser_load_yaml_from_fd()
  • netplan_parser_load_keyfile()
  • netplan_netdef_get_id()
  • _netplan_netdef_get_embedded_switch_mode() #for internal use only

If any strings are to be returned, we want the caller to do all allocation and free’ing of any memory needed, so that libnetplan itself can write the output strings to an existing buffer. The function would return a ‘ssize_t’ value that would either indicate the number of bytes written to the buffer, or an error code in the negative range. If the provided buffer is too small, the function should return a well-defined error code. This way (lib-)netplan and its caller are most independent and the memory management/allocation technology can be changed independently without worrying about breaking any callers (think GLib vs Glibc memory allocation, or switching over netplan to another language like Rust, using another way of memory allocation).

When we need to return complex errors beyond the error code, we use the GError** pattern from GLib, but with a custom type (that will for now internally be typedef’d to GError to avoid breaking ABI), with the following functions available to the user:

  • void netplan_error_clear(NetplanError** error);
  • ssize_t netplan_error_message(NetplanError* error, char* buf, size_t buf_size);
  • uint64 netplan_error_code(NetplanError* error); (uint64 to encode both domain and code)

Object and API definition

Netplan objects to be used publicly/externally.

  • NetplanNetDefinition API

    • Any data is supposed to be retrieved via accessor functions (getters/setters), libnetplan’s data structs shall never be used externally.
    • netplan_netdef_get_id()
    • netplan_netdef_get_type()
    • netplan_netdef_get_backend()
    • netplan_netdef_get_bridge/bond/peer/link/sriov_link()
      • Shall return a pointer to a netdef instead of a string
    • netplan_netdef_get_filepath()
      • e.g. YAML filename the the netdef data has been read from
    • netplan_netdef_get_output_filename()
      • e.g. for NM keyfile, to be written
    • Matching logic → do not expose all the individual data (like: mac, ifname, drivers), but only provide a “match_interface(netdef, ifname)” function, returning True/False if the given netdef matches that interface
      • netplan_netdef_get_set_name()
      • netplan_netdef_match_interface()
      • netplan_netdef_has_match()
    • For internal use only, i.e. not part of the public API (e.g. netplan CLI / python bindings):
      • _netplan_netdef_get_critical/optional()
      • _netplan_netdef_get_vf_count/vlan_filter/embedded_switch_mode/delay_rebind
    • netplan_netdef_write_yaml()
  • NetplanParser API

    • A class to be used for parsing a bunch of netplan YAML files from the filesystem, carrying a hashmap of “parsed_defs” amongst others. The netdef data in this class has not been validated and might contain invalid user input
    • netplan_parser_new()
    • netplan_parser_reset()
    • netplan_parser_clear()
    • netplan_parser_load_yaml()
    • netplan_parser_load_keyfile()
  • NetplanState API

    • A class to be used for importing the NetplanParser class’ results, validating it to avoid user input/configuration conflicts. It contains an ordered list of all netdefs and a hashmap of netplan_id → pointers to the very same netdef data.
    • netplan_state_new()
    • netplan_state_import_parser_results()
    • netplan_state_reset()
    • netplan_state_clear()
    • netplan_state_get_backend()
    • netplan_state_get_netdefs_size()
    • netplan_state_get_netdef()
    • netplan_state_dump_yaml()
    • netplan_state_finish_nm_write()
    • netplan_state_finish_ovs_write()
    • netplan_state_finish_sriov_write()

Additional API for generic netplan utils

  • netplan_delete_connection()
  • netplan_generate()
  • netplan_get_id_from_nm_filename()
  • netplan_get_filename_by_id()

Cleanup of public headers

At some point after the new API has been widely available, we want to bump the SONAME and break the old/legacy API, this is when we want to clean up the legacy API by dropping the old symbols. Until we reach that point, we have a compatibility wrapper that is located in src/abi_compat.c and ./abicompat.lds to accommodate for global variables and legacy API functions/symbols.

  • Remove from public headers write_netplan_conf (but keep it in abi compat)
  • Remove everything below “Old API comments”
  • Also check the “After soname bump (ABI break)” section of ./TODO for more cleanup hints

Further Information

2 Likes

@slyon - Subiquity and Cloud-Init both aspire to validate our respective YAML end-to-end, and Netplan is a part of that.

Does this specification have thoughts on how that would be achieved? While calls to libnetplan could be part of that, there are also people interested in crafting this YAML offline - what do you think?

CC @chad.smith

1 Like

I don’t know the particulars of your implementation, but assuming your netplan config is embedded as multiple YAML objects throughout your documents, you’d need to first split off those netplan stanzas into their own files, and then do something along those lines:

int[] fds; // For the sake of argument, you've put the netplan YAML blurbs in memfd files
size_t fds_size;
NetplanError *error = NULL;
NetplanState *state = netplan_state_new();
NetplanParser *parser = netplan_parser_new();
for (size_t i = 0; i < fds_size; i++) {
    // That one gives you a first pass validation
    if (!netplan_parser_load_yaml_from_fd(parser, fds[i], &error)) {
        handle_error(error);
       goto cleanup;
    }
}
if (!netplan_state_import_parser_results(state, parser, &error) {
    handle_error(error);
}
cleanup:
netplan_error_free(error);
netplan_parser_clear(&parser);
netplan_state_clear(&state);

And writing this I note that netplan_parser_load_yaml_from_fd() is not in this spec. @slyon do we want to remedy this?

2 Likes