From: rbu9fe <106606487+rbu9fe@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:24:51 +0000 (+0100) Subject: feat: Add lldpctl C++ wrapper (#693) X-Git-Tag: 1.0.19~5 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=d3072c0612f22400c1263c45b5e1aaf02ed5e56d;p=thirdparty%2Flldpd.git feat: Add lldpctl C++ wrapper (#693) * feat: Add lldpctl C++ wrapper * fix: Call the new lldpctl_watch_sync_unblock() on watch termination * fix: Category name --- diff --git a/README.md b/README.md index c753b669..1e0e47a3 100644 --- a/README.md +++ b/README.md @@ -429,7 +429,9 @@ To embed lldpd into an existing system, there are two points of entry: parse its output for integration and automation purpose. Another way is to use SNMP support. A third way is to write your own controller using `liblldpctl.so`. Its API is described in - `src/lib/lldpctl.h`. The custom binary protocol between + `src/lib/lldpctl.h`. For C++, there is a wrapper class in + `src/lib/lldpctl.hpp` with automatic lifetime management of + underlying resouces. The custom binary protocol between `liblldpctl.so` and `lldpd` is not stable. Therefore, the library should always be shipped with `lldpd`. On the other hand, programs using `liblldpctl.so` can rely on the classic ABI rules. diff --git a/src/lib/lldpctl.hpp b/src/lib/lldpctl.hpp new file mode 100644 index 00000000..7eac77e7 --- /dev/null +++ b/src/lib/lldpctl.hpp @@ -0,0 +1,542 @@ +/** + * @brief lldpctl API C++ wrapper + */ + +#pragma once + +/* *** includes ***************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* *** defines ****************************************************************/ + +#ifndef __stringify_1 +#define __stringify_1( x ) #x +#endif + +#ifndef __stringify +#define __stringify( x ) __stringify_1( x ) +#endif + +#define CHECK_LLDP_GENERIC( failed, __call, ... ) \ + do \ + { \ + if ( failed( __call ) ) \ + { \ + __VA_ARGS__; \ + } \ + } while ( 0 ) + +#define FAILED_NULL( p ) ( ( p ) == nullptr ) +#define CHECK_LLDP_P( __call, conn ) \ + CHECK_LLDP_GENERIC( \ + FAILED_NULL, __call, const auto _rc_{ lldpctl_last_error( conn ) }; if ( LLDPCTL_NO_ERROR != _rc_ ) { \ + throw std::system_error( std::error_code( _rc_, LldpErrCategory() ), "'" __stringify(__call) "' failed" ); \ + } ) + +#define FAILED_NEGATIVE( v ) ( ( v ) < 0 ) +#define CHECK_LLDP_N( __call, conn ) \ + CHECK_LLDP_GENERIC( \ + FAILED_NEGATIVE, __call, const auto _rc_{ lldpctl_last_error( conn ) }; \ + throw std::system_error( std::error_code( _rc_, LldpErrCategory() ), "'" __stringify(__call) "' failed" ); ) + +#define CHECK_LLDP_N2( pre, __call, conn ) \ + CHECK_LLDP_GENERIC( \ + FAILED_NEGATIVE, __call, if ( pre ) { \ + const auto _rc_{ lldpctl_last_error( conn ) }; \ + throw std::system_error( std::error_code( _rc_, LldpErrCategory() ), "'" __stringify(__call) "' failed" ); \ + } ) + +/* *** type declarations ******************************************************/ + +/* *** exported interfaces ****************************************************/ + +namespace lldpcli +{ +namespace literals +{ +/** + * @brief Operator to define std::byte literals. + * + * Example: auto byte{ 0x01_b }; + */ +consteval std::byte operator"" _b( unsigned long long int value ) +{ + return static_cast( value ); +} +} // namespace literals + +/** + * @brief LLDP error category. + */ +class LldpErrCategory : public std::error_category +{ +public: + const char *name() const noexcept override + { + return "lldpctl"; + } + + std::string message( int ev ) const override + { + return ::lldpctl_strerror( static_cast( ev ) ); + } +}; + +/** + * @brief Wrapper class for @p lldpctl_atom_t with automatic lifetime management. + */ +class LldpAtom +{ +public: + using vector = std::vector; + using span = std::span; + + /** + * @brief Construct a new Lldp Atom. + * + * @param atom The atom provided by the library. + * @param inc_ref_cnt If @p true increment the atom's reference count as ownership is not implicitly transferred from the caller. + * @param conn The connection the atom belongs to. Used to extend the connection lifetime as the atom can't live without it. + * May be @p nullptr in which case the connection's lifetime can't be extended by the atom object (e.g. as the connection is owned by the library). + * @param parent The optional parent atom. Used to extend the parent atom's lifetime as the child atom can't live without it. + */ + explicit LldpAtom( + lldpctl_atom_t *atom, bool inc_ref_cnt, const std::shared_ptr &conn, + std::unique_ptr parent = nullptr ) + : atom_( atom ) + , conn_( conn ) + , parent_( std::move( parent ) ) + { + if ( inc_ref_cnt ) + { + ::lldpctl_atom_inc_ref( atom_ ); + } + } + + ~LldpAtom() + { + if ( atom_ ) + { + ::lldpctl_atom_dec_ref( atom_ ); + } + } + + LldpAtom( const LldpAtom &other ) noexcept + : atom_( other.atom_ ) + , conn_( other.conn_ ) + , parent_( other.parent_ ? std::make_unique( *other.parent_ ) : nullptr ) + { + ::lldpctl_atom_inc_ref( atom_ ); + } + + LldpAtom &operator=( const LldpAtom &other ) + { + if ( this != &other ) + { + atom_ = other.atom_; + conn_ = other.conn_; + parent_ = other.parent_ ? std::make_unique( *other.parent_ ) : nullptr; + ::lldpctl_atom_inc_ref( atom_ ); + } + + return *this; + } + + LldpAtom( LldpAtom &&other ) noexcept + : atom_( other.atom_ ) + , conn_( other.conn_ ) + , parent_( std::move( other.parent_ ) ) + { + other.atom_ = nullptr; + other.conn_ = nullptr; + other.parent_ = nullptr; + } + + LldpAtom &operator=( LldpAtom &&other ) noexcept + { + if ( this != &other ) + { + atom_ = other.atom_; + conn_ = other.conn_; + parent_ = std::move( other.parent_ ); + other.atom_ = nullptr; + other.conn_ = nullptr; + other.parent_ = nullptr; + } + + return *this; + } + + LldpAtom GetPort() const + { + lldpctl_atom_t *atom; + CHECK_LLDP_P( atom = ::lldpctl_get_port( atom_ ), conn_.get() ); + return LldpAtom{ atom, false, conn_ }; + } + + std::optional GetAtom( lldpctl_key_t key ) const + { + auto atom{ ::lldpctl_atom_get( atom_, key ) }; + return atom ? std::make_optional( atom, false, conn_ ) : std::nullopt; + } + + LldpAtom CreateAtom() const + { + lldpctl_atom_t *atom; + CHECK_LLDP_P( atom = ::lldpctl_atom_create( atom_ ), conn_.get() ); + /* Store the parent atom to increase its reference count so that it remains living as long as the child lives. */ + return LldpAtom{ atom, false, conn_, std::make_unique( *this ) }; + } + + void SetAtom( lldpctl_key_t key, const LldpAtom &child ) + { + CHECK_LLDP_P( ::lldpctl_atom_set( atom_, key, child.atom_ ), conn_.get() ); + } + + std::list GetAtomList( lldpctl_key_t key ) const + { + lldpctl_atom_t *it; + CHECK_LLDP_P( it = ::lldpctl_atom_get( atom_, key ), conn_.get() ); + + std::list list; + lldpctl_atom_t *atom; + lldpctl_atom_foreach( it, atom ) + { + list.emplace_back( atom, true, conn_ ); + } + + return list; + } + + template + auto GetValue( lldpctl_key_t key ) const + { + if constexpr ( std::is_same_v || std::is_same_v ) + { + const auto str{ ::lldpctl_atom_get_str( atom_, key ) }; + return str ? std::make_optional( str ) : std::nullopt; + } + else if constexpr ( std::is_same_v ) + { + const auto value{ ::lldpctl_atom_get_int( atom_, key ) }; + return lldpctl_last_error( lldpctl_atom_get_connection( atom_ ) ) == LLDPCTL_NO_ERROR + ? std::make_optional( value ) + : std::nullopt; + } + else if constexpr ( std::is_same_v || std::is_same_v ) + { + size_t length{ 0 }; + const auto buffer{ ::lldpctl_atom_get_buffer( atom_, key, &length ) }; + + if ( buffer ) + { + auto it{ reinterpret_cast( buffer ) }; + return T{ it, it + length }; + } + else + { + return T{}; + } + } + else + { + static_assert( always_false_::value, "Unsupported type" ); + } + } + + template + void SetValue( lldpctl_key_t key, const T &data ) + { + if constexpr ( + std::is_same_v> || std::is_same_v> ) + { + CHECK_LLDP_P( + ::lldpctl_atom_set_str( atom_, key, data.has_value() ? data->data() : nullptr ), conn_.get() ); + } + else if constexpr ( std::is_same_v ) + { + CHECK_LLDP_P( ::lldpctl_atom_set_int( atom_, key, data ), conn_.get() ); + } + else if constexpr ( std::is_same_v || std::is_same_v ) + { + CHECK_LLDP_P( + ::lldpctl_atom_set_buffer( atom_, key, reinterpret_cast( data.data() ), data.size() ), + conn_.get() ); + } + else + { + static_assert( always_false_::value, "Unsupported type" ); + } + } + +private: + template + struct always_false_ : std::false_type + { + }; + + lldpctl_atom_t *atom_; + std::shared_ptr conn_; + std::unique_ptr parent_; +}; + +/** + * @brief Wrapper class for @p lldpctl_conn_t with automatic lifetime management. + */ +class LldpCtl +{ +public: + explicit LldpCtl() + { + if ( !conn_ ) + { + throw std::system_error( + std::error_code( LLDPCTL_ERR_NOMEM, LldpErrCategory() ), "Could not create lldpctl connection." ); + } + } + + ~LldpCtl() = default; + + LldpCtl( const LldpCtl &other ) = default; + + LldpCtl &operator=( const LldpCtl &other ) + { + if ( this != &other ) + { + conn_ = other.conn_; + } + + return *this; + } + + LldpCtl( LldpCtl &&other ) noexcept + : conn_( other.conn_ ) + { + other.conn_ = nullptr; + } + + LldpCtl &operator=( LldpCtl &&other ) noexcept + { + if ( this != &other ) + { + conn_ = other.conn_; + other.conn_ = nullptr; + } + + return *this; + } + + LldpAtom GetConfiguration() const + { + lldpctl_atom_t *atom; + CHECK_LLDP_P( atom = ::lldpctl_get_configuration( conn_.get() ), conn_.get() ); + return LldpAtom{ atom, false, conn_ }; + } + + std::list GetInterfaces() const + { + const auto &it{ ::lldpctl_get_interfaces( conn_.get() ) }; + + std::list list; + lldpctl_atom_t *atom; + lldpctl_atom_foreach( it, atom ) + { + list.emplace_back( atom, true, conn_ ); + } + + return list; + } + + std::optional GetInterface( std::string_view if_name ) const + { + for ( const auto &interface : GetInterfaces() ) + { + if ( interface.GetValue( lldpctl_k_interface_name ) == if_name ) + { + return interface; + } + } + + return std::nullopt; + } + + LldpAtom GetLocalChassis() const + { + lldpctl_atom_t *atom; + CHECK_LLDP_P( atom = ::lldpctl_get_local_chassis( conn_.get() ), conn_.get() ); + return LldpAtom{ atom, false, conn_ }; + } + + LldpAtom GetDefaultPort() const + { + lldpctl_atom_t *atom; + CHECK_LLDP_P( atom = ::lldpctl_get_default_port( conn_.get() ), conn_.get() ); + return LldpAtom{ atom, false, conn_ }; + } + + static std::string_view get_default_transport() noexcept + { + return ::lldpctl_get_default_transport(); + } + + static std::map> KeyGetMap( lldpctl_key_t key ) noexcept + { + std::map> map; + + lldpctl_map_t *entry{ ::lldpctl_key_get_map( key ) }; + while ( entry->string ) + { + map.try_emplace( entry->string, entry->value ); + ++entry; + } + + return map; + } + +private: + std::shared_ptr conn_{ ::lldpctl_new( nullptr, nullptr, this ), &::lldpctl_release }; +}; + +/** + * @brief Wrapper for change callback registration. + * + * @tparam X Context pointer type for the general optional callback passed to the constructor. + * @tparam Y Context pointer type for the interface specific callbacks passed to @ref RegisterInterfaceCallback. + */ +template +class LldpWatch +{ +public: + template + using ChangeCallback = const std::function; + + /** + * @brief Construct a new Lldp Watch object. + * + * @param callback Optional callback to trigger on remote changes. + * Additionally, interface specific callbacks can be registered using @ref RegisterInterfaceCallback. + * @param ctx Optional context passed to @p callback. + */ + explicit LldpWatch( const std::optional> &callback = std::nullopt, const X *ctx = nullptr ) + : general_callback_( + callback.has_value() ? std::make_optional( std::make_pair( *callback, const_cast( ctx ) ) ) + : std::nullopt ) + { + if ( !conn_ ) + { + throw std::system_error( + std::error_code( LLDPCTL_ERR_NOMEM, LldpErrCategory() ), "Could not create lldpctl connection." ); + } + + CHECK_LLDP_N( + ::lldpctl_watch_callback2( conn_, &LldpWatch::WatchCallback, static_cast( this ) ), conn_ ); + + thread_ = std::jthread{ [this]( std::stop_token stop ) + { + while ( !stop.stop_requested() ) + { + CHECK_LLDP_N2( !stop.stop_requested(), ::lldpctl_watch( conn_ ), conn_ ); + } + } }; + } + + ~LldpWatch() + { + if ( conn_ ) + { + thread_.request_stop(); + ::lldpctl_watch_sync_unblock( conn_ ); + thread_.join(); + ::lldpctl_release( conn_ ); + } + } + + LldpWatch( const LldpWatch & ) = delete; + LldpWatch &operator=( const LldpWatch & ) = delete; + LldpWatch( LldpWatch &&other ) = delete; + LldpWatch &operator=( LldpWatch && ) = delete; + + /** + * @brief Register an interface specific callback on remote changes. + * + * @param if_name The local interface to monitor. + * @param callback Callback to trigger on remote changes. + * @param ctx Optional context passed to @p callback. + * @param trigger_init It @p true then @p callback is invoked during registration for all existing neighbors. + */ + void RegisterInterfaceCallback( + const std::string &if_name, ChangeCallback callback, const Y *ctx, bool trigger_init = false ) + { + const auto interface{ LldpCtl().GetInterface( if_name ) }; + if ( !interface.has_value() ) + { + throw std::system_error( + std::error_code( LLDPCTL_ERR_NOT_EXIST, LldpErrCategory() ), + "Couldn't find interface '" + if_name + "'" ); + } + + std::scoped_lock lock{ mutex_ }; + + /** + * Note: + * There's a race one way or the other - we decided to accept the one that the neighbor + * changes between reading it and registering the callback. + */ + if ( trigger_init ) + { + for ( const auto &neighbor : interface->GetPort().GetAtomList( lldpctl_k_port_neighbors ) ) + { + callback( if_name, lldpctl_change_t::lldpctl_c_added, *interface, neighbor, const_cast( ctx ) ); + } + } + + interface_callbacks_.try_emplace( if_name, std::make_pair( callback, const_cast( ctx ) ) ); + } + +private: + static void WatchCallback( lldpctl_change_t change, lldpctl_atom_t *interface, lldpctl_atom_t *neighbor, void *p ) + { + /* These LldpAtoms don't extend the lifetime of the underlying connection as it's owned by the library. */ + LldpAtom interface_atom{ interface, true, nullptr }; + LldpAtom neighbor_atom{ neighbor, true, nullptr }; + + const auto if_name{ *interface_atom.GetValue( lldpctl_k_interface_name ) }; + + auto self{ static_cast *>( p ) }; + + std::scoped_lock lock{ self->mutex_ }; + + if ( self->general_callback_.has_value() ) + { + auto [callback, ctx]{ self->general_callback_.value() }; + callback( if_name, change, interface_atom, neighbor_atom, ctx ); + } + + if ( auto it{ self->interface_callbacks_.find( if_name ) }; it != self->interface_callbacks_.end() ) + { + auto [callback, ctx]{ it->second }; + callback( if_name, change, interface_atom, neighbor_atom, ctx ); + } + } + + lldpctl_conn_t *conn_{ ::lldpctl_new( nullptr, nullptr, this ) }; + std::jthread thread_; + std::mutex mutex_; + const std::optional, X *>> general_callback_; + std::map, Y *>, std::less<>> interface_callbacks_; +}; + +} // namespace lldpcli \ No newline at end of file