Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

devlink

Inspect and configure Linux devlink devices.

let (connection, handle) = devlink::new_connection()?;
tokio::spawn(connection);

for device in handle.devices().await? {
    let info = handle.info(&device.handle()).await?;
    println!("{} {:?}", device.handle(), info.running);
}

devlink is a generic netlink family, so the family id is resolved at runtime. Wire format lives in netlink-packet-devlink.

What works

API devlink(8) equivalent Privileges
handle.devices() devlink dev show none
handle.info(&h) devlink dev info CAP_NET_ADMIN
handle.eswitch(&h) devlink dev eswitch show CAP_NET_ADMIN
handle.set_eswitch(&h, ..) devlink dev eswitch set CAP_NET_ADMIN
handle.params(&h) devlink dev param show CAP_NET_ADMIN
handle.set_param(&h, ..) devlink dev param set CAP_NET_ADMIN
handle.ports(..) devlink port show none
handle.health_reporters(..) devlink health show CAP_NET_ADMIN
handle.reload(&h, ..) devlink dev reload CAP_NET_ADMIN
handle.set_port_function(&p, ..) devlink port function set CAP_NET_ADMIN
handle.rates(..) devlink port function rate show CAP_NET_ADMIN
handle.set_rate(&a, ..) devlink port function rate set CAP_NET_ADMIN
handle.add_rate_node(..) devlink port function rate add CAP_NET_ADMIN
handle.del_rate_node(..) devlink port function rate del CAP_NET_ADMIN
handle.health_recover(..) devlink health recover CAP_NET_ADMIN
handle.set_health_reporter(..) devlink health set CAP_NET_ADMIN
handle.health_diagnose(..) devlink health diagnose CAP_NET_ADMIN
handle.health_dump(..) devlink health dump show CAP_NET_ADMIN
handle.clear_health_dump(..) devlink health dump clear CAP_NET_ADMIN
handle.health_test(..) devlink health test CAP_NET_ADMIN
handle.flash_update(..) devlink dev flash CAP_NET_ADMIN
handle.traps(..) devlink trap show CAP_NET_ADMIN
handle.set_trap(..) devlink trap set CAP_NET_ADMIN
handle.trap_groups(..) devlink trap group show CAP_NET_ADMIN
handle.set_trap_group(..) devlink trap group set CAP_NET_ADMIN
handle.trap_policers(..) devlink trap policer show CAP_NET_ADMIN
handle.set_trap_policer(..) devlink trap policer set CAP_NET_ADMIN
new_monitor_connection() devlink monitor none
handle.set_notify_filter(..) (no equivalent) none

Verified against a ConnectX-6, an Intel i40e and netdevsim, field for field against devlink(8).

eswitch

use devlink::{EswitchChange, EswitchMode};

handle.set_eswitch(&h, EswitchChange::mode(EswitchMode::SwitchDev)).await?;

Only the fields set in EswitchChange are sent, so this does not clobber settings you did not ask about.

Changing mode is disruptive. The driver tears down and rebuilds its netdevices, and on mlx5 switchdev creates a representor per VF. A verified side effect: the device's RDMA index changes too, because the eswitch flip re-registers the ib_devicemlx5_0 went from index 0 to index 12 across one round trip. Anything caching RDMA device indices needs to re-resolve.

cargo run --example eswitch_switchdev -- pci/0000:02:00.0 does a full legacy → switchdev → legacy cycle, restoring the original mode on every exit path including failures.

Parameters

use devlink::{ParamCmode, ParamValueInput};

handle.set_param(&h, "enable_roce", ParamCmode::Driverinit,
                 ParamValueInput::Bool(true)).await?;

ParamValueInput exists because the wire encoding has rules that are easy to violate and produce a bare EINVAL:

  • integers must be sent at their exact width — a u32 parameter rejects a 4-byte value sent as anything else
  • strings must be NUL-terminated inside the attribute
  • a boolean is a flag: present with zero length means true, absent means false. Sending a bool with a one-byte payload is rejected.

The same asymmetry applies when reading, and it bit us: ParamValue::as_bool returns bool, not Option<bool>, because the kernel only emits PARAM_VALUE_DATA for a true boolean. Treating the missing attribute as "unknown" rather than false made esw_multiport look unreadable.

A driverinit value does not take effect until the driver reloads.

cargo run --example param_roundtrip -- pci/0000:02:00.0 reads every parameter and writes it back unchanged, exercising the full set path across all three cmodes and every value type without altering the device:

pci/0000:02:00.0: 17 parameter(s)
  max_macs                     Driverinit = 128   (written back ok)
  enable_roce                  Driverinit = true  (written back ok)
  flow_steering_mode           Runtime = smfs     (written back ok)
  esw_multiport                Runtime = false    (written back ok)
  cqe_compress_type            Permanent = balanced (written back ok)

17 written back, 0 skipped
0 parameter(s) differ from before -- expected 0

Reload

use devlink::ReloadAction;

let result = handle.reload(&h, ReloadAction::DriverReinit, None).await?;
println!("{:?}", result.actions_performed);   // [DriverReinit]

This is what makes a driverinit parameter take effect.

Disruptive. DriverReinit tears down and re-instantiates the driver's entities. Verified on a ConnectX-6: netdevices go away and come back, and the device's RDMA index moved from 12 to 13 across one reload. Anything caching netdev or RDMA indices must re-resolve afterwards.

Two things the API handles for you:

  • The action is always sent, even though the kernel defaults to DriverReinit. devlink_nl_reload_doit only replies with RELOAD_ACTIONS_PERFORMED when the request named an action or a limit — omit it and you get a bare ack with no way to tell what happened.
  • A limit is a struct nla_bitfield32, eight bytes: a value and a selector, and a bit only counts when set in both. It is not a u32 of flags; sending four bytes gets EINVAL. The bit asserted is the enum value's positionDEVLINK_RELOAD_LIMIT_NO_RESET is 1, so the wire bit is BIT(1) = 2. Bitfield32::set and bit_of do this correctly.

Setting a driverinit parameter for real

The sequence a control plane needs is set → reload → verify, because a driverinit value is not consumed until the driver reinitialises:

pci/0000:02:00.0  enable_roce (driverinit)
  original value:      true
  setting to false ...
  reads back as:       Some(false)
  reloading ...
  actions performed:   [DriverReinit]
  after reload:        Some(false)
  value survived the reload

cargo run --example param_reload_verify -- <handle> <param> does exactly that, then restores and reloads again.

Reload into another network namespace

reload_into(.., Some(Netns::Pid(pid))) moves the device. This is the only way to put an mlx5 RDMA device in a namespace: devlink_reload_netns_change rewrites the devlink instance's net between reload_down and reload_up, and mlx5_ib then allocates its ib_device with

ib_alloc_device_with_net(mlx5_ib_dev, ib_dev, mlx5_core_net(mdev));
/* mlx5_core_net(dev) == devlink_net(priv_to_devlink(dev)) */

mlx5 is the only driver in the tree wired up this way — which is why ib_alloc_device_with_net exists at all.

Whether the RDMA device follows depends entirely on ib_core.netns_mode. _ib_alloc_device discards the requested net while the system is in shared mode:

net = ib_devices_shared_netns ? &init_net : net;

Both measured on the same hardware, same code, different boot parameter:

devlink instance RDMA device
default (netns_mode=1, shared) moves stays in init_net
ib_core.netns_mode=0 (exclusive) moves moves with it

With exclusive mode:

BEFORE init_net:  devlink 02:00.0: 1,  rdma mlx5_0: 1
AFTER  init_net:  devlink 02:00.0: 0,  rdma mlx5_0: 0   <- both left
AFTER  target ns: devlink 02:00.0: 1,  rdma mlx5_0: 1   <- both arrived

So a devlink reload is a complete, working way to place an mlx5 RDMA device in a namespace — and it is the better of the two routes, because it sidesteps ib_device_set_netns_put's disassociate_ucontext requirement entirely by destroying and re-creating the device rather than migrating it. Both routes still need the boot parameter. See the rdma crate's README.

Ports and health reporters

let ports = handle.ports(Some(&h)).await?;              // or None for all
let reporters = handle.health_reporters(Some(&h)).await?;

Note where mlx5 puts things: the PCI device has no ports, they belong to the auxiliary device.

pci/0000:02:00.0            0 port(s), reporters: fw, fw_fatal, vnic
auxiliary/mlx5_core.eth.0   1 port  (65535, physical, eth, enp2s0f0np0)
                            reporters: tx, rx

Both match devlink port show and devlink health show exactly. An empty port list for a PCI handle is a correct answer, not a bug.

esw_multiport is not a stored setting

It looks like an ordinary boolean parameter, but mlx5 implements it as a live view of LAG state:

static int mlx5_devlink_esw_multiport_get(...)
{ ctx->val.vbool = mlx5_lag_is_mpesw(dev); return 0; }

static int mlx5_devlink_esw_multiport_set(...)
{ ... return mlx5_lag_mpesw_enable(dev); }

and mlx5_lag_mpesw_queue_work opens with

struct mlx5_lag *ldev = mlx5_lag_dev(dev);
if (!ldev)
    return 0;      /* success, having done nothing */

So the set returns success and silently does nothing when the card has no LAG device — confirmed here with both PFs in switchdev mode:

setting pci/0000:02:00.0 to switchdev ...  now Some(SwitchDev)
setting pci/0000:02:00.1 to switchdev ...  now Some(SwitchDev)
enabling esw_multiport on pci/0000:02:00.0 ...
  set returned ok, reads back as Some(false)

Always read back after setting this oneOk(()) tells you nothing.

MPESW does not need link, despite appearances

The natural assumption is that the ports must be operationally up. The code says otherwise — mlx5_lag_enable_mpesw gates on:

if (mlx5_eswitch_mode(dev0) != MLX5_ESWITCH_OFFLOADS ||
    !MLX5_CAP_PORT_SELECTION(dev0, port_select_flow_table) ||
    !MLX5_CAP_GEN(dev0, create_lag_when_not_master_up) ||
    !mlx5_lag_check_prereq(ldev) ||
    !mlx5_lag_shared_fdb_supported(ldev))
	return -EOPNOTSUPP;

No link state anywhere, and one of the capabilities is literally named create_lag_when_not_master_up. The carrier requirement people hit belongs to bond-driven LAG, where mlx5_lag_do_bond reacts to netdev events — a different entry point.

The real blocker is upstream of both: mlx5_lag_mpesw_queue_work returns 0 if mlx5_lag_dev(dev) is NULL. Measured on a BlueField-3 B3240 (SEPARATED_HOST, LAG_RESOURCE_ALLOCATION=PRE_ALLOCATION, both ports ETH), with both PFs in switchdev and both netdevs enslaved to an 802.3ad bond: mlx5's LAG layer emitted no dmesg at all and esw_multiport stayed false. No LAG device exists, so neither loopback nor cabling would change the outcome — the question is why mlx5_lag_is_supported() declines, which needs a capability dump this toolchain cannot produce.

Port function: IPsec, RoCE, MAC, state

IPsec offload is not a devlink parameter, despite looking like one. It is a capability bit inside DEVLINK_ATTR_PORT_FUNCTION:

use devlink::{FnCaps, PortFunctionChange};

handle.set_port_function(&port, PortFunctionChange::cap(
    FnCaps { ipsec_packet: true, ..Default::default() },
    true,
)).await?;

Three traps here, all of which cost a debugging round:

  1. The nested set has its own attribute namespace. HW_ADDR is 1 and STATE is 2 — the same numbers as BUS_NAME and DEV_NAME in the main space. The kernel parses it with devlink_function_nl_policy, so it gets its own PortFunctionAttr type. Most devlink nests are not like this.
  2. PORT_FUNCTION needs NLA_F_NESTED. devlink(8) sends nla_type=NLA_F_NESTED|0x91; without the flag the kernel answers EINVAL with no hint.
  3. Select only the capability you are changing. Caps are a bitfield32 and the kernel dispatches per selected bit, so naming one the device does not implement fails the whole request — even when the value you asked for is the one it already has. PortFunctionChange::cap selects exactly one.

Port functions only exist on VF/SF ports, which only exist in switchdev mode. A physical port reports no function; that is a correct answer.

Verified on a BlueField-3: roce toggles off and back on, and ipsec_packet returns EOPNOTSUPP — the same answer devlink(8) gives, since this device does not implement packet-mode IPsec.

Rate objects

use devlink::{RateAddr, RateChange};

handle.add_rate_node(&h, "tenant_a",
    RateChange::default().tx_share(500_000_000).tx_max(1_000_000_000)).await?;

handle.set_rate(&RateAddr::Node { handle: h.clone(), name: "tenant_a".into() },
    RateChange::default().tx_max(2_000_000_000)).await?;

Rates are in bytes per second. A leaf is addressed by port index and a node by name — sending the wrong one gets EINVAL, so RateAddr keeps them distinct.

Like port functions, rate objects only appear in switchdev mode. Verified: two leaves discovered for two VF ports, a node created, retuned and deleted.

Packet traps

use devlink::{TrapAction, TrapGroupChange, TrapPolicerChange};

handle.set_trap(&h, "blackhole_route", TrapAction::Trap).await?;
handle.set_trap_group(&h, "l2_drops",
    TrapGroupChange::default().action(TrapAction::Drop).policer(1)).await?;
handle.set_trap_policer(&h, 1,
    TrapPolicerChange::default().rate(2000).burst(128)).await?;

A trap names a reason the hardware may punt, drop or mirror a packet. Groups exist so one action or policer applies to many traps; a policer is a token bucket limiting how much trapped traffic reaches the CPU.

Traps are a switch-ASIC feature — mlxsw, prestera, netdevsim. Most NICs register none, and an empty list is a correct answer. This mlx5 registers two.

set_trap returns Ok(()) when it does nothing

The kernel refuses to change the action of a non-drop trap, because dropping an exception or control trap would break forwarding or the control plane. It reports that refusal by returning 0 with an extack message:

if (trap_item->action != trap_action &&
    trap_item->trap->type != DEVLINK_TRAP_TYPE_DROP) {
        NL_SET_ERR_MSG(extack, "Cannot change action of non-drop traps. Skipping");
        return 0;
}

Measured on netdevsim, both through this crate and devlink(8):

drop trap source_mac_is_multicast: drop -> trap
  reads back as: trap                         <- took effect
  restored to:   drop

exception trap fid_miss: asking for drop
  request returned Ok
  reads back as: trap
  UNCHANGED -- the kernel skips non-drop traps and still reports success

devlink(8) prints Warning: Cannot change action of non-drop traps. Skipping. and also exits 0. It can say that because it reads netlink extack, which netlink-packet-core 0.8 does not model at all — ErrorMessage carries only a code and the original header, and nothing sets NETLINK_EXT_ACK on the socket. So this crate cannot surface the reason, only the silence. Check [Trap::trap_type] before calling, or read the trap back. This is the same shape as esw_multiport: Ok(()) is not confirmation.

Policer 0 is not a policer

It is how devlink spells "none". Verified against the kernel both ways:

$ devlink trap policer set netdevsim/netdevsim1 policer 0 rate 1000
Error: Device did not register this trap policer.        # ENOENT

$ devlink trap group set netdevsim/netdevsim1 group l3_drops policer 0
$ devlink trap group show netdevsim/netdevsim1 group l3_drops
  name l3_drops generic true                             # unbound, not bound

So 0 can never name a policer — it is reserved by construction. That fact lives in no_policer(), which names the intent at the call site, rather than in the type:

TrapGroupChange::default()               // policer: None    -> untouched
TrapGroupChange::default().policer(id)   //          Some(id) -> bound
TrapGroupChange::default().no_policer()  //          Some(0)  -> unbound

Option<NonZeroU32> was tried here and reverted. It defended a case the kernel does not produce — TRAP_POLICER_ID is only emitted when a policer is actually bound, so Some(0) never arrives — and the compiler flagged four call sites, none of which was a bug. What it did buy was an asymmetry: the read side could use Option<NonZeroU32> but the write side has three states, not two, so it needed a separate PolicerBinding enum. That made the crate answer one question two ways, when RateChange::parent had already answered it once: an empty parent name means "detach", and .parent() / .no_parent() carry the meaning. .policer() / .no_policer() now match it.

Verified round trip:

group l2_drops: no policer bound
  bound policer 1 -> Some(1)
  no_policer()    -> None

Verified

Against netdevsim (14 traps, 6 groups, 3 policers) and the mlx5 (2 traps, 1 group, no policers), field for field against devlink(8) — including fid_miss generic false, the one row whose flag differs from every other and so the canary for a decoder that defaults it to true.

A group action fans out, on both: set group action trap -> 2/2 now trap on the mlx5, 1/1 on netdevsim, each restored afterwards.

Two more nests with their own numbering

Traps brought two more of the PORT_FUNCTION hazard, and these are worse because they are numbered from zero:

nest child 0 child 1 child 2
STATS RX_PACKETS u64 RX_BYTES u64 RX_DROPPED u64
TRAP_METADATA IN_PORT flag FA_COOKIE flag
main namespace UNSPEC BUS_NAME string DEV_NAME string

Decoding a stats nest with DevlinkAttr does not mislabel a field — it reads eight bytes of counter as a NUL-terminated string. They get their own types, StatsAttr and TrapMetadataAttr, and a test asserts the misread really would happen so the reason for the split does not get lost.

cargo run --example traps -- [handle] [--go].

Monitor

let (connection, handle, messages) = devlink::new_monitor_connection().await?;
tokio::spawn(connection);

let mut notifications = devlink::notifications(messages);
while let Some(notification) = notifications.next().await {
    println!("{notification}");
}

Every devlink change is published on one multicast group, DEVLINK_MCGRP_CONFIG (named "config"). Two things make subscribing harder than it looks:

  1. The group has no fixed number. devlink is a generic netlink family, so the group id is allocated at registration and has to be resolved by name, like the family id. new_monitor_connection is async for this reason — it does a round trip before it can subscribe.
  2. netlink-proto never binds the socket. It relies on the kernel binding implicitly on the first sendto. An unbound socket is not in the kernel's listener tables, so it receives no multicast however many groups it joinsadd_membership alone is silently useless. The same bug bit the rdma crate's monitor. Bind first, then join.

Verified against devlink(8) by churning a netdevsim instance: 68 notifications, 68 notifications, same objects in the same order — device, parameters, region, three trap policers, six trap groups, fourteen traps, ports, then the whole thing torn down in reverse.

[netdevsim/netdevsim1] dev
[netdevsim/netdevsim1] param max_macs
[netdevsim/netdevsim1] region dummy
[netdevsim/netdevsim1] trap-policer 1
[netdevsim/netdevsim1] trap-group l2_drops
[netdevsim/netdevsim1] trap igmp_query action Mirror
[netdevsim/netdevsim1] port 1 netdev eni1np2
[del netdevsim/netdevsim1] trap source_mac_is_multicast action Drop
[del netdevsim/netdevsim1] dev

A write that changes nothing is silent

devlink dev param set with the value a parameter already holds succeeds and notifies nothing. devlink(8) sees the same silence, so this is kernel behaviour, not a decoding gap:

write notifications
max_macs 128 → 64 → 128 2
max_macs 128 → 128 0

This is a stream of changes, not of writes. Do not use it to confirm a request was applied — read the value back.

Kernel-side filtering

handle.set_notify_filter(Some(&h), None).await?;   // one device
handle.set_notify_filter(None, None).await?;       // clear

devlink(8) has no equivalent — its monitor receives everything and discards the rest in userspace. DEVLINK_CMD_NOTIFY_FILTER_SET (kernel 6.9+) attaches the filter to the socket, so the process is never woken at all. Measured over the same netdevsim churn:

socket notifications
unfiltered 68
filtered to netdevsim/netdevsim1 34 (the half after the filter was set)
filtered to pci/0000:02:00.0 0

The filter is per-socket kernel state, which is why it has to be set through the handle that new_monitor_connection returns rather than an ordinary one. Older kernels answer EOPNOTSUPP; listening unfiltered is still correct.

cargo run --example monitor -- [handle].

Health diagnose and dump: formatted messages

for reporter in handle.health_reporters(None).await? {
    let tree = handle.health_diagnose(&reporter.addr()).await?;
    println!("{tree}");
}

DEVLINK_ATTR_FMSG is a flat token stream that reconstructs into a tree, and one NEST_END closes any kind of nest, so decoding it is a parser rather than a fold:

FmsgObjNestStart
  FmsgPairNestStart
    FmsgObjName("Syndrome")
    FmsgObjValueType(U8) FmsgObjValueData([0])
  FmsgNestEnd
FmsgNestEnd

A pair's value can itself be an object or an array, which is what makes it recursive. [Fmsg] is the resulting tree.

A value can be cut in half at a message boundary

This one cost a debugging round and is the reason fw worked while tx did not. A value is two nla_put calls, a type then a data. When the type fits the remaining skb and the data does not, the kernel writes the type, fails, and re-emits the whole item in the next message -- leaving an orphan behind:

1610 FmsgObjName("pc")
1611 FmsgObjValueType(U32)   <- last token of message 3, no data follows
1612 FmsgObjValueType(U32)   <- first token of message 4
1613 FmsgObjValueData([0, 0, 0, 0])

Measured on an mlx5 tx reporter: 8703 tokens across 17 messages, three of which ended in an orphan. Small reporters never split, so fw and vnic parse fine without handling it and the bug only appears once a diagnose is a few kilobytes. The parser takes the last type of a run, since a real value has exactly one.

Tokens must also be concatenated across messages before parsing, or a nest that straddles a split never closes. Fmsg::from_messages does both.

Verified

Against an mlx5 ConnectX-6, cross-checked with devlink health diagnose:

=== pci/0000:02:00.0 vnic  state Some(Healthy) errors 0 ===
  vNIC env counters:
    total_error_queues: 0
    invalid_command: 1
    icm_consumption: 34022

=== auxiliary/mlx5_core.eth.0/65535 tx  state Some(Healthy) errors 0 ===
  Common Config:
    SQ:
      stride size: 64
      size: 1024
      ts_format: FRC

Every value matches, checked by multiset rather than by eye -- 63 each of sqn, cqn, eqn, irqn, txq ix and channel ix, identical to devlink(8).

fw_fatal stores a firmware crdump: an array of ~3.7KB binary chunks, several megabytes in total, spread over hundreds of messages. Display truncates a long binary to 32 bytes and a length, because rendering it in full turns a status line into 39MB of hex; the bytes stay in the Binary variant.

Reporters are addressed by scope

fw, fw_fatal and vnic belong to the device; tx and rx belong to a port, and naming one without its index is rejected:

$ devlink health diagnose auxiliary/mlx5_core.eth.0 reporter tx
kernel answers: Invalid argument

ReporterAddr carries the scope and HealthReporter::addr() builds the right one from a dump entry, so the distinction cannot be got wrong by accident.

Nested devlink instances

for device in handle.devices().await? {
    println!("{} -> {:?}", device.handle(), device.nested);
}
pci/0000:02:00.0 -> [auxiliary/mlx5_core.eth.0]

The section on ports notes that mlx5 puts ports on the auxiliary device rather than the PCI one. DEVLINK_ATTR_NESTED_DEVLINK is how to follow that link programmatically instead of guessing the name.

Device::stats carries the reload counters devlink dev show prints, which survive across reloads:

pci/0000:02:00.0   reloaded 2x  action Some(DriverReinit) limit None

Firmware flash

handle.flash_update(&h, FlashUpdate::new("firmware.bin")).await?;

This writes to the device's flash, and a failed write can leave a card unbootable. It has deliberately never been pointed at real hardware here.

Two things that are easy to get wrong:

  • The kernel opens the file, not this process. file_name is resolved by request_firmware against the kernel's search path, which /sys/module/firmware_class/parameters/path prepends to. A file that exists in your working directory is not found.
  • The ack does not arrive until the flash finishes -- minutes on real hardware. Progress is published as notifications, so pair this with the monitor rather than waiting blind.

Testing it without risking a card

netdevsim implements flash_update as a software stub with no firmware behind it, so the entire protocol can be exercised at zero risk. Point the kernel's firmware loader at a scratch directory, flash a file whose contents are ignored, then put the path back:

flashing netdevsim/netdevsim1 with fake.bin ...
  notify: [netdevsim/netdevsim1] flash started
  notify: [netdevsim/netdevsim1] flash Preparing to flash
  notify: [netdevsim/netdevsim1] flash Flashing 0/50000
  ... 51 progress notifications ...
  notify: [netdevsim/netdevsim1] flash Flashing 50000/50000
  notify: [netdevsim/netdevsim1] flash Flash select
  notify: [netdevsim/netdevsim1] flash Flashing done
  notify: [netdevsim/netdevsim1] flash finished
flash returned Ok

That covers the request encoding, all three status attributes, the notification decoding and the long-running-doit behaviour. What it does not cover is a real driver's flash implementation -- but that part is the driver's, not this crate's.

There is deliberately no flash example in this repository: an example that writes firmware is an attractive nuisance.

Not yet covered

Shared buffer (18 commands, 15 attributes), dpipe (4 commands and 36 attributes -- the largest remaining surface), linecards, regions, resources and selftests. Probed against the hardware here, every one of them is either EOPNOTSUPP or an empty list: they are switch-ASIC and modular-chassis features. Those groups are 63 of the 93 unmodelled attributes, so coverage for NIC-class devices is considerably more complete than the raw count suggests.

DEVLINK_CMD_PORT_PARAM_* (4 commands) is dead rather than deferred: no driver registers port parameters, the dump returns nothing, and iproute2 has no command for them.

Netlink extack is the one gap that cuts across everything already here. The kernel attaches a human-readable reason to many refusals — including the two "returns success, does nothing" cases this crate has hit — and netlink-packet-core 0.8 models none of it. Adding it means setting NETLINK_EXT_ACK on the socket, parsing NLMSGERR_ATTR_MSG out of the ack, and threading it through Handle::request's return, so it belongs upstream rather than here.

About

Inspect and configure Linux devlink devices

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages