Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sys Runtime Configuration Registry #19895

Open
wants to merge 118 commits into
base: master
Choose a base branch
from

Conversation

LasseRosenow
Copy link
Contributor

@LasseRosenow LasseRosenow commented Aug 22, 2023

Contribution description

RIOT Runtime Configuration Registry

This is a continuation of the effort previously done in #10622, but its architecture has dramatically changed since then.

Abstract

This PR implements a system level runtime configuration system for RIOT.

A runtime configuration system is in charge of providing a mechanism to set and get the values of Configuration Parameters that are used during the execution of the firmware, as well as a way to persist these values. Runtime configurations are deployment-specific and can be changed on a per node basis.
Appropriate management tools could also enable the configuration of nodes.

Examples of runtime configurations are:

  • Transmission duty cycles
  • Sensor thresholds
  • Security credentials
  • System state variables
  • RGB LED colors

These parameters might have constraints, like a specific order to be applied (due to interdependencies) or value boundaries.

The main advantages of having such a system are:

  • Easy to apply per-node configuration during deployment
  • No need to implement a special mechanism for per-node configurations during firmware updates (only in the case of migration), as the parameters persist.
  • Common interface for modules to expose their runtime Configuration Parameters and handle them
  • Common interface for storing Configuration Parameters in non-volatile
    storage devices

Design

Architecture

The proposed architecture, as shown below, is formed by one or more Applications or Configuration Managers and the RIOT Registry.
The RIOT Registry acts as a common interface to access Runtime Configurations and store them in non-volatile storage.
All runtime configurations can be accessed either from the RIOT application using the provided RIOT Registry interfaces or through the interfaces exposed by the Configuration Managers.
A RIOT Application may interact with a Configuration Manager in order to modify access control rules or enable different exposed interfaces.

Path Based Configuration Managers

These Configuration Managers are a simple representation of the default configuration structure of the RIOT Registry.
They use either the int_path or the string_path Registry extension to expose the parameters using a path.

Custom Schema Based Configuration Managers

These Configuration Managers have their own configuration structure (custom predefined object models etc.) and can not automatically be mapped to / from the RIOT Registry itself.
To make them work, a custom mapping module needs to be implemented, which maps each Configuration Parameter from the registry to the correct format of the Configuration Manager.

design_architecture

Namespaces and Storages

The RIOT Registry interacts with RIOT modules via Configuration Schemas, and with non-volatile storages via Storages.
This way the functionality of the RIOT Registry is independent of the functionality of a module or storage implementation.
It is possible to get or set the values of Configuration Parameters.
It is also possible to transactionally apply configurations or export their values to a buffer or print them.
To persist Configuration Values, it is possible to store them in non-volatile storages.

Any mechanism of security (access control, encryption of configurations) is not directly in the scope of the Registry but in the Configuration Managers and the specific implementations of the Configuration Schemas and Storages.

The graphic below shows an example of two Configuration Namespaces (SYS and APP).
The APP namespace contains a application specific My app Configuration Schema and the SYS namespace specifies a WLAN and a LED Strip Configuration Schema.
The application My app uses the custom My app Configuration Schema to expose custom Configuration Parameters to the RIOT Registry and the drivers WS2812, SK6812 and UCS1903 contain instances of
the LED Strip Configuration Schema to expose common LED Strip Configuration Parameters.

Also, there are two Storages available: MTD and VFS.
The MTD Storage internally uses the RIOT MTD driver and the VFS Storage internally uses the RIOT VFS module.

design_components

Components

The RIOT Registry is split into multiple components as can be seen in the graphic below:

Registry Core

This component holds the most basic functionality of the RIOT Registry.
It allows to set and get Configuration Values, transactionally commit them to make the changes come into effect and export all Configuration Parameters that exist in a given Configuration Namespace, Configuration Schema or Configuration Group.

Furthermore it is possible to add Configuration Namespaces or Configuration Schema Instances.

Registry Namespace

The Configuration Namespaces such as SYS or APP and their respective Configuration Schemas are not part of the Registry itself.
It is possible to add custom Configuration Namespaces depending on the given needs.

Storage

The Storage component provides an interface to load Configuration Values from a persistent Storage implementation or to save the current Registry configuration to it.

Registry Storage

The implementations of a Storage such as VFS or MTD are not part of the Registry itself and can be switched out with implementations that are most suitable to the given needs.

Integer Path

The int_path component provides helper functions that convert a path of up to 4 integer values to the respective pointer of a Configuration Namespace, Configuration Schema, Configuration Schema Instance, Configuration Group or Configuration Parameter and the other way around.

The structure of an integer configuration path is the following:
namespace_id / schema_id / instance_id / group_id | parameter_id

For example:

Path Result
0 Namespace Object
0 / 1 Namespace and Schema Object
0 / 1 / 0 Namespace, Schema and Instance Object
0 / 1 / 0 / 3 Namespace, Schema, Instance and Group or Parameter Object

String Path

The string_path component provies helper functions that convert a string path to the respective pointer of a Configuration Namespace, Configuration Schema, Configuration Schema Instance, Configuration Group or Configuration Parameter and the other way around.

The structure of a string configuration path is the following:
namespace_name / schema_name / instance_id (/ group_name)* / parameter_name.

The amount of path items is flexible, so the path could only consist of the namespace_name or only the namespace_name and the schema_name and so on.

For example:\

Path Result
sys Namespace Object
sys / temperature_pressure_humidity Namespace and Schema Object
sys / temperature_pressure_humidity / 0 Namespace, Schema and Instance Object
sys / temperature_pressure_humidity / 0 / calibration Namespace, Schema, Instance and Group Object
sys / temperature_pressure_humidity / 0 / calibration / humidity Namespace, Schema, Instance, Group and Parameter Object
sys / temperature_pressure_humidity / 0 / last_reading_timestamp Namespace, Schema, Instance and Parameter Object

design_registry

API

The graphic below shows the API of the RIOT Registry.
The top shows the Core API to manage Configuration Parameters.
On the right-hand side are functions to set and get Configuration Parameters, transactionally commit them and export them to a buffer or terminal.
On the left-hand side are setup functions to add Configuration Namespaces and Configuration Schema Instances to the Registry.

The bottom shows the storage API to manage the persistance of Configuration Parameters.
The left-hand side shows functions to load” and save Configuration Parameters to and from the persistent Storage.
The right-hand side shows functions to add Storage Sources (for reading) and to set a Storage Destination (for writing).
The Registry can have multiple Storage Sources, but always only one Storage Destination.
This allows to migrate from an old Storage to a new one.

The functionality of these functions is explained in the following paragraphs.

api_structure

Core API

Get

A Configuration Value can be retrieved using the registry_get function.
The function takes the Configuration Schema Instance, the Configuration Parameter and a registry_value_t pointer (to return the value) as its arguments.

int registry_get(
    const registry_instance_t *instance,
    const registry_parameter_t *parameter,
    registry_value_t *value,
);

Set

A Configuration Value can be set using the registry_set function.
The function takes the Configuration Schema Instance, the Configuration Parameter, a void* buffer and the buffer size as its arguments.

The buffer must contain the value in its correct c-type.
If the Registry expects a u8, but a u16 is provided, the operation will fail.
Furthermore the registry can specify constraints like minimum and maximum values and an array of allowed or forbidden values.
If these constraints are not fulfilled, then the operation will fail as well.

int registry_set(
    const registry_instance_t *instance,
    const registry_parameter_t *parameter,
    const void *buf,
    const size_t buf_len,
);

Commit

Once the value(s) of one or multiple Configuration Parameter(s) are changed by the registry_set function, they still need to be committed, so that the new values are taken into effect.

Configuration Parameter(s) can be committed using the registry_commit function.
In this case the Registry provides multiple commit functions to allow committing in varying degrees.
The provided functions are registry_commit, this function commits every Configuration Parameter currently available in the Registry, registry_commit_namespace, this function commits every Configuration Parameter within the given Configuration Namespace, registry_commit_schema, this function commits every Configuration Parameter within the given Configuration Schema, registry_commit_instance, this function commits every Configuration Parameter within the given Configuration Schema Instance, registry_commit_group, this function commits every Configuration Parameter within the given Configuration Group and registry_commit_parameter, this function commits only a single Configuration Parameter.

When a Configuration Parameter is committed, it will be passed on to the commit_cb handler of the Configuration Schema Instance, provided by the Driver / Module that needs runtime configuration.
This way the Driver / Module gets notified, when the Configuration Parameter has been committed and can apply the changes accordingly.

int registry_commit(void);
int registry_commit_namespace(const registry_namespace_t *namespace);
int registry_commit_schema(const registry_schema_t *schema);
int registry_commit_instance(const registry_instance_t *instance);
int registry_commit_group(
    const registry_instance_t *instance,
    const registry_group_t *group,
);
int registry_commit_parameter(
    const registry_instance_t *instance,
    const registry_parameter_t *parameter,
);

Export

Some times it is convenient to have a way to see what Configuration Namespaces, Configuration Schemas, Configuration Schema Instances, Configuration Groups or Configuration Parameters are available within our current RIOT Registry deployment.
To get this information there is the registry

The RIOT Registry allows exporting these objects using the registry_export function.
In this case the Registry provides multiple export functions to allow exporting in varying degrees.
The provided functions are registry_export, this function exports every Configuration Object currently available in the Registry, registry_export_namespace, this function exports every Configuration Object within the given Configuration Namespace, registry_export_schema, this function exports every Configuration Object within the given Configuration Schema, registry_export_instance, this function exports every Configuration Object within the given Configuration Schema Instance, registry_export_group, this function exports every Configuration Object within the given Configuration Group and registry_export_parameter, this function exports only a single Configuration Parameter.

When a Configuration Parameter is exported, it will be passed on to the export_cb handler provided as an argument of each registry_export* function.
This way inside of the export_cb function we can process the result and for example print all available Configuration Parameter to the console.

int registry_export(
    const registry_export_cb_t export_cb,
    const uint8_t recursion_depth,
    const void *context,
);
int registry_export_namespace(
    const registry_namespace_t *namespace,
    const registry_export_cb_t export_cb,
    const uint8_t recursion_depth,
    const void *context,
);
int registry_export_schema(
    const registry_schema_t *schema,
    const registry_export_cb_t export_cb,
    const uint8_t recursion_depth,
    const void *context
);
int registry_export_instance(
    const registry_instance_t *instance,
    const registry_export_cb_t export_cb,
    const uint8_t recursion_depth,
    const void *context,
);
int registry_export_group(
    const registry_instance_t *instance,
    const registry_group_t *group,
    const registry_export_cb_t export_cb,
    const uint8_t recursion_depth,
    const void *context,
);
int registry_export_parameter(
    const registry_instance_t *instance,
    const registry_parameter_t *parameter,
    const registry_export_cb_t export_cb,
    const void *context,
);

Add Namespaces to the Registry

To be able to use Configuration Schemas and their Parameters etc. it is necessary to add a Configuration Namespace that holds the required Configuration Schemas to the Registry.

This is possible using the REGISTRY_ADD_NAMESPACE macro, providing the name of the Configuration Namespace and a pointer to a registry_namespace_t object as arguments.

#define REGISTRY_ADD_NAMESPACE(_name, _namespace)

Add Configuration Schema Instances to the Registry

To implement runtime configuration functionality into a Module / Driver, it is necessary to add a Configuration Schema Instance of the needed Configuration Schema to the Registry.

This is possible using the registry_add_schema_instance function, providing the Configuration Schema and the Configuration Schema Instance as arguments.

int registry_add_schema_instance(
    const registry_schema_t *schema,
    const registry_instance_t *instance,
);

Storage API

Load from Storage

It is often needed to load Configuration Parameters from a non-volatile Storage device.
For example when a device restarts after a shutdown.

This is possible using the registry_load function.
Internally the registry_load function calls the load callback of a registered Storage Source.
That function takes a registry_storage_instance_t and a load_cb_t as its arguments.
The the registry_storage_instance_t contains data such as the mount point.
the load_cb_t is used to tell the Registry about found Configuration Values.

This load_cb_t function takes a pointer to a Configuration Schema Instance a pointer to a Configuration Parameter, a value void * buffer and its size as arguments.
Inside its load function, the Storage Source searches its persistent storage device for Configuration Values.
If a Configuration Value is found, the Storage Source calls the load_cb_t function and provides the necessary arguments.

int registry_load(void);

Save to Storage

To save Configuration Values to a non-volatile Storage device, the Registry provides multiple functions:
The function registry_save saves all available Configuration Parameters within the RIOT Registry to the storage.
The function registry_save_namespace saves all available Configuration Parameters within the specified Configuration Namespace to the storage.
The function registry_save_schema saves all available Configuration Parameters within the specified Configuration Schema to the storage.
The function registry_save_instance saves all available Configuration Parameters within the specified Configuration Schema Instance to the storage.
The function registry_save_group saves all available Configuration Parameters within the specified Configuration Group to the storage.
The function registry_save_parameter saves a single provided Configuration Parameters to the storage.

Internally these functions call the save handler of the Storage Destination for each Configuration Parameter that has to be saved to storage.
The save handler of the Storage Destination takes a Storage Instance providing data such as the mount point, a Configuration Schema Instance, a Configuration Parameter and a Configuration Value as its arguments.
These values provide the same information base that is necessary to load them using the load_cb_t as explained in the previous section.

The way how the Storage stores these values internally is not specified.

int registry_save(void);
int registry_save_namespace(const registry_namespace_t *namespace);
int registry_save_schema(const registry_schema_t *schema);
int registry_save_instance(const registry_instance_t *instance);
int registry_save_group(
    const registry_instance_t *instance,
    const registry_group_t *group,
);
int registry_save_parameter(
    const registry_instance_t *instance,
    const registry_parameter_t *parameter,
);

Add Storage Sources

To be able to load configurations from Storage it is necessary to add a Storage Source that can read Configuration Values from a persistent storage device.

This is possible using the REGISTRY_ADD_STORAGE_SOURCE macro, providing a pointer to a registry_storage_instance_t object as its argument.

#define REGISTRY_ADD_STORAGE_SOURCE(_storage_instance)

Set the Storage Destination

To be able to write configurations to Storage it is necessary to add a Storage Destination that can write Configuration Values to a persistent storage device.

This is possible using the REGISTRY_SET_STORAGE_DESTINATION macro, providing a pointer to a registry_storage_instance_t object as its argument.

#define REGISTRY_SET_STORAGE_DESTINATION(_storage_instance)

Comparison to Apache Mynewt Config

While originally our work on the RIOT Registry was heavily inspired by Apache Mynewt Config, it has since evolved to provide features such as Type Safety, make String Paths optional, introduce Integer Paths and provide a more modular Pointer-Based API.

The table below shows the difference between Apache Mynewt Config and the proposed RIOT Registry.

The Idea here is not to talk down Apache Mynewt Config, as its simplicity of course has its own advantages, but to point out more clearly how our solution differs to it.

Feature Mynewt Config RIOT Registry
Pointer based API
Integer Path based API
String Path based API
Shared Configuration Schemas with instances
Nested configuration groups / parameters
Parameter types (string, int8, uint32, float, ...)
Internal parameter value format String Any (defined by schema)
Persistent configuration
Transactional commits

External Configuration Managers

CLI

The only available Configuration Manager in this PR is a CLI.
It can be tried in the example under examples/registry.

The CLI uses int paths separated by /.
E.g.: 0/0/0/0.

CoAP API

The idea is to use the registry_int and/or registry_path module to provide a simple CoAP API.

Get

The get function could be mapped to the CoAP GET function.

Set

The set function could be mapped to the CoAP PUT function.

Commit

This is more tricky.
Possible solutions could be to provide a /commit suffix or prefix at the end or beginning of a path, to tell the registry, that this is a commit operation.

Export

The export function could be implemented using a CoAP GET function using /export as a suffix or prefix of the path.
CoAP would then return one response containing all the requested objects for example structured using CBOR.

LwM2M

A LwM2M integration would require to write a mapping between each LwM2M Object to the respective RIOT Registry Configuration Schemas.

In this case every LwM2M set operation would immediately trigger a registry_commit as LwM2M does not provide a commit operation.
This is not a drawback, as LwM2M allows to set multiple values at the same time, thus covering the same use-case as the registry_commit function.

Testing procedure

Testing commands unter tests/unittests:

make tests-registry term

make tests-registry_storage term

make tests-registry_storage_heap term

make tests-registry_storage_vfs term

make tests-registry_int_path term

make tests-registry_string_path term

Issues/PRs references

See also #10622
See also #19557

@github-actions github-actions bot added Area: tests Area: tests and testing framework Area: build system Area: Build system Area: sys Area: System Area: examples Area: Example Applications Area: Kconfig Area: Kconfig integration labels Aug 22, 2023
@LasseRosenow LasseRosenow changed the title Pr runtime configuration registry Sys Runtime Configuration Registry Aug 22, 2023
@benpicco benpicco requested review from fabian18 and bergzand August 23, 2023 09:30
@waehlisch
Copy link
Member

thanks for this very careful PR description, including the feature matrix comparison!

Add storage instance as a parameter
Migrate to local test namespace
Copy link
Member

@maribu maribu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating the PR and providing the metrics.

I see that the LoC have now gone down to 7K. This is still rather large for reviewing. Would it be possible if you split out a PR that does not provide any storage backend yet? E.g. just the base features + test + an example that uses the shell interface, no persistence yet?

I added some nitpicks inline.

const char * const name; /**< String describing the instance. */
#endif /* CONFIG_REGISTRY_ENABLE_META_NAME */
const void * const data; /**< Struct containing all configuration parameters of the schema. */
const registry_schema_t *schema; /**< Configuration Schema that the Schema Instance belongs to. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const registry_schema_t *schema; /**< Configuration Schema that the Schema Instance belongs to. */
const registry_schema_t *schema; /**< Configuration Schema that the registry Instance belongs to. */

Comment on lines +49 to +51
switch (scope)
{
case REGISTRY_COMMIT_INSTANCE:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
switch (scope)
{
case REGISTRY_COMMIT_INSTANCE:
switch (scope) {
case REGISTRY_COMMIT_INSTANCE:

There are a number of deviations from the coding convention regarding code formatting. This is not a big deal and coding style is IMO purely about personal preference, but keeping things consistent within the code base does (at least me) help a lot when reading code.

I won't add further comments on style, since this is best left to uncrustify or clang-format.

rsource "sys/Kconfig"
rsource "tests/Kconfig"

endif # MODULE_REGISTRY_NAMESPACE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last line of this file is not terminated with a single \n. I think uncrustify or clang-format will also correct this. If not, e.g. visual studio code with the config now in the master branch should also do so on save.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: build system Area: Build system Area: doc Area: Documentation Area: examples Area: Example Applications Area: Kconfig Area: Kconfig integration Area: sys Area: System Area: tests Area: tests and testing framework
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants