Skip to content

Mctp bridge support #71

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

faizana-nvidia
Copy link
Contributor

This PR aims to introduce ALLOCATE_ENDPOINT_ID message support along with MCTP Bridge endpoint into the existing peer structure.

@jk-ozlabs
Copy link
Member

Thanks for the contribution! I'll get to a proper review shortly.

I have some pending changes that rework a lot of the peer, link and network allocation mechanisms. That shouldn't affect your code too much, but I'll request a rebase once that is merged.

@jk-ozlabs jk-ozlabs self-assigned this Apr 25, 2025
@faizana-nvidia
Copy link
Contributor Author

Thanks for the contribution! I'll get to a proper review shortly.

I have some pending changes that rework a lot of the peer, link and network allocation mechanisms. That shouldn't affect your code too much, but I'll request a rebase once that is merged.

Sure no problem

Copy link
Member

@jk-ozlabs jk-ozlabs left a comment

Choose a reason for hiding this comment

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

So the main design point here is how we're handling the pool allocations. It looks like your particular use-case is around static allocations, which I'll focus on here.

As I mentioned in the dbus changes, we cannot add arguments without further version-compatiblity changes. After a bit of chatting with the team, I think a better approach would be to add a new dbus call to explicitly allocate a bridge and a predefined pool (which would include the pool size). Perhaps something like:

AllocateBridgeStatic(addr: ay, pool_start: y, pool_size: y)
  • where the Set Endpoint ID response must match the expected pool size.

(we would also want purely-dynamic pools to be allocated from SetupEndpoint and friends, but that would result in a dynamic pool allocation. This dynamic pool would be defined either by a toml config option, or via a new TMBO dbus interface. However, we can handle those later, I think)

Would that work?

Contains one interface (lladdr 0x10, local EID 8), and one endpoint (lladdr
0x1d), that reports support for MCTP control and PLDM.
Contains two interface (lladdr 0x10, local EID 8), (lladdr 0x11, local EID 10) with one endpoint (lladdr
0x1d), and MCTP bridge (lladdr 0x1e, pool size 2), that reports support for MCTP control and PLDM.
Copy link
Member

Choose a reason for hiding this comment

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

Please don't alter the default sysnet unless it's needed by a significant number of tests (which in this case, it is not). Just set up the test fixtures default as needed for your new test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see, so better to update the current interface (lladdr 0x10, local EID 8) with pool size and update pool size numbered eids to the network simulating a bridge rather than creating a new one?

Copy link
Member

Choose a reason for hiding this comment

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

Yep! Just update the interface/endpoints/etc in the individual test case.

@jk-ozlabs
Copy link
Member

In general, can you add a bit more of an explanation / rationale as part of your commit messages, instead of just log output? There is some good guidance for commit messages up in the "Patch formatting and changelogs" section of https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/process/5.Posting.rst

@jk-ozlabs
Copy link
Member

We'll also need to consider the routing setup for bridged endpoints. Ideally we would:

  1. create a route for the bridge itself, plus a neighbour entry with the appropriate physical address data
  2. create a range route for the allocated endpoint pool, using the bridge as a gateway for that range (ie, no neighbour entry)

the issue is that there is no kernel support for (2) at present: we need some kernel changes to implement gateway routes. It is possible to create "somewhat-fake" routes for those endpoints, using a neighbour table entry for each (bridged) peer that uses the bridge phys address, but that's a bit suboptimal. I'd prefer not to encode that hack into mctpd if possible.

I do have a todo for the kernel changes necessary for that, sounds like I should get onto it!

@santoshpuranik
Copy link

We'll also need to consider the routing setup for bridged endpoints. Ideally we would:

1. create a route for the bridge itself, plus a neighbour entry with the appropriate physical address data

2. create a range route for the allocated endpoint pool, using the bridge as a gateway for that range (ie, no neighbour entry)

the issue is that there is no kernel support for (2) at present: we need some kernel changes to implement gateway routes. It is possible to create "somewhat-fake" routes for those endpoints, using a neighbour table entry for each (bridged) peer that uses the bridge phys address, but that's a bit suboptimal. I'd prefer not to encode that hack into mctpd if possible.

I do have a todo for the kernel changes necessary for that, sounds like I should get onto it!

IIUC, 1 is what we can achieve with the tools we have today, right? For ex: add route to the bridge itself and then mctp route add <downstream eid> via <bridge net if>, essentially adding a neighbour table entry? Would this not continue to work as from TMBO point-of-view all packets go via the bridge route.

When you say sub-optimal, are you referring to the neighbour lookup that happens in net/mctp/route.c? Noob question, how does a gateway impl make that faster?

When is the gateway support in kernel for MCTP nets planned? We can help if you have a design in mind.

@jk-ozlabs
Copy link
Member

Hi Santosh,

IIUC, 1 is what we can achieve with the tools we have today, right?

Yes, but it requires a lot of workaround to set up.

For ex: add route to the bridge itself and then mctp route add <downstream eid> via <bridge net if>, essentially adding a neighbour table entry?

That isn't adding a neighbour table entry though; just a route. USB is a little different in that there are no neighbour table entries required, because there is no physical addressing.

For a bridge, using this scheme would require:

  1. adding the route to the bridge EID
  2. adding the neighbour entry for the bridge EID
  3. adding individual routes for each EID in the EID pool
  4. adding individual fake neighbour table entries for each EID in the EID pool, which would (incorrectly) represent that the EID has a specific physical address (ie., that of the bridge)

(for USB, we don't need (2) or (4), but that's purely a property of the transport type. We would need those to be supported in mctpd to allow other transport types like i2c).

This would work, but it's messy.

When you say sub-optimal, are you referring to the neighbour lookup that happens in net/mctp/route.c?

No, the neighbour lookups happen in net/mctp/neigh.c.

Noob question, how does a gateway impl make that faster?

Not so much faster, more tidier. With a gateway route, we would require:

  1. adding the route to the bridge EID
  2. adding the neighbour entry for the bridge EID
  3. adding one range route for the entire EID pool, referencing the bridge EID as the gateway

No fake neighbour table entries are required - since the kernel just looks up the gateway physical address from the gateway's neighbour table entry.

When is the gateway support in kernel for MCTP nets planned?

I have it done - will push a development branch shortly.

@jk-ozlabs
Copy link
Member

I have it done - will push a development branch shortly.

https://github.com/CodeConstruct/linux/tree/dev/forwarding

@santoshpuranik
Copy link

Hi Jeremy,

Thank you for the detailed response.

Hi Santosh,

IIUC, 1 is what we can achieve with the tools we have today, right?

Yes, but it requires a lot of workaround to set up.

For ex: add route to the bridge itself and then mctp route add <downstream eid> via <bridge net if>, essentially adding a neighbour table entry?

That isn't adding a neighbour table entry though; just a route. USB is a little different in that there are no neighbour table entries required, because there is no physical addressing.

For a bridge, using this scheme would require:

1. adding the route to the bridge EID

2. adding the neighbour entry for the bridge EID

3. adding individual routes for each EID in the EID pool

4. adding individual fake neighbour table entries for each EID in the EID pool, which would (incorrectly) represent that the EID has a specific physical address (ie., that of the bridge)

Ack, I see something like I2C would need a PHY address.

When you say sub-optimal, are you referring to the neighbour lookup that happens in net/mctp/route.c?

No, the neighbour lookups happen in net/mctp/neigh.c.

Ack. I should have said the neigh_lookup call that happens in route.c!

Noob question, how does a gateway impl make that faster?

Not so much faster, more tidier. With a gateway route, we would require:

1. adding the route to the bridge EID

2. adding the neighbour entry for the bridge EID

3. adding one range route for the entire EID pool, referencing the bridge EID as the gateway

No fake neighbour table entries are required - since the kernel just looks up the gateway physical address from the gateway's neighbour table entry.

Thank you, that does seem cleaner.

@jk-ozlabs
Copy link
Member

And for the userspace changes, my dev/gateway branch here:

https://github.com/CodeConstruct/mctp/tree/dev/gateway

@santoshpuranik
Copy link

@jk-ozlabs : I think we agree that mctpd has to poll all allocated endpoints with a Get Endpoint ID periodically. I think the first thing we'd need to enable in order to do that is to make MCTP requests and responses asynchronous. Do you have a design in mind to make MCTP requests async (like via a request queue per allocated endpoint)?

@jk-ozlabs
Copy link
Member

jk-ozlabs commented Jun 3, 2025

I think we agree that mctpd has to poll all allocated endpoints with a Get Endpoint ID periodically

Just as a clarification - not all endpoints, but EIDs within allocated endpoint ranges, which have not yet been enumerated. And this is assuming we expect mctpd to automatically enumerate those bridged devices. I think the latter is reasonable, but we don't have a specific design point around that yet.

With that in mind, yes, we probably want to make that async, as those requests are likely to not have a response, and therefore we're at worst-case waiting time.

In terms of design: we probably don't want a struct peer to be created for those endpoints, as they don't strictly exist as proper peers at that stage. I think a minimal-impact approach may be to keep a set of the allocated (but not-yet-enumerated) ranges, and periodically send the Get Endpoint ID requests.

We don't necessarily need to keep much state for that polling mechanism (ie, between request and response) - receiving a Get Endpoint ID response for anything in that range would trigger the enumeration process.

@santoshpuranik
Copy link

I think we agree that mctpd has to poll all allocated endpoints with a Get Endpoint ID periodically

Just as a clarification - not all endpoints, but EIDs within allocated endpoint ranges, which have not yet been enumerated.

Wouldn't we also want to poll enumerated endpoints under the bridge to determine when they "went away"?

In terms of design: we probably don't want a struct peer to be created for those endpoints, as they don't strictly exist as proper peers at that stage. I think a minimal-impact approach may be to keep a set of the allocated (but not-yet-enumerated) ranges, and periodically send the Get Endpoint ID requests.

We don't necessarily need to keep much state for that polling mechanism (ie, between request and response) - receiving a Get Endpoint ID response for anything in that range would trigger the enumeration process.

Ack. How periodically do you think we should check? Same as the logic for determining when to set endpoint state as degraded (TReclaim/2)?

@jk-ozlabs
Copy link
Member

Wouldn't we also want to poll enumerated endpoints under the bridge to determine when they "went away"?

No, and we don't do that with directly-attached endpoints either. The current philosophy is that we don't care if an endpoint disappears, until some application calls Recover. If there's no application using the endpoint, then no need to monitor for its presence.

[I'm okay with revisiting this, or handling bridged endpoints differently, if there's a compelling argument for doing so]

How periodically do you think we should check?

Treclaim/2 seems a bit too often to me, but might be fine as a starting point. I suspect that an ideal approach would be to poll more regularly when a bridge pool is initially allocated, then reduce frequency. However, let's not complicate the initial implementation too much here, and just use a configurable constant.

@jk-ozlabs
Copy link
Member

jk-ozlabs commented Jun 3, 2025

.. and speaking of Recover, we might need to revisit how we handle that for bridged endpoints, as a MCTP-level recovery operation probably isn't applicable as a directly-attached device (in the same manner, at least). CC @amboar.

@santoshpuranik
Copy link

No, and we don't do that with directly-attached endpoints either.

So we have a case where we will have to call allocate endpoint ID on the bridge device when not all of its downstream devices are available. In such a case, how do you think we can determine when those downstream EIDs become available unless we poll?

@jk-ozlabs
Copy link
Member

how do you think we can determine when those downstream EIDs become available unless we poll

I am suggesting we poll. Just that we then stop polling once we enumerate the endpoint.

@santoshpuranik
Copy link

how do you think we can determine when those downstream EIDs become available unless we poll

I am suggesting we poll. Just that we then stop polling once we enumerate the endpoint.

Ah, ack, then.

@amboar
Copy link
Contributor

amboar commented Jun 11, 2025

.. and speaking of Recover, we might need to revisit how we handle that for bridged endpoints, as a MCTP-level recovery operation probably isn't applicable as a directly-attached device (in the same manner, at least). CC @amboar.

It will need some rework as currently it assumes the peer is a neighbour and uses physical addressing for Get Endpoint ID. We still want a mechanism to establish the loss of a non-neighbour peer though. I think Recover is fine for that. We need to use some message for polling, and despite the absurdity I think Get Endpoint ID is also fine for that, just we can't use physical addressing if the peer is behind a bridge. The observed behaviour of Recover would be the same - if the peer is responsive then the D-Bus object remains exposed, or if it's unresponsive then the object is removed. The difference between a peer being unresponsive as opposed to not having yet been assigned an address cannot be determined across the bridge, so in that case we skip the substance of the recovery operation (EID re-assignment). That's a responsibility of the bridge node anyway.

@jk-ozlabs
Copy link
Member

Thanks for that, Andrew.

There might be some commonality between the peers undergoing (non-local) recovery, and those EIDs that are behind a bridge, but not-yet enumerated. If a Recover of a non-local endpoint fails (ie, the Get Endpoint ID commands involved in the Recover process all timeout), then we should return that EID to the "allocated but not yet enumerated" EID set, which means we will continue to send periodic Get Endpoint ID commands (perhaps on a less frequent basis though).

The same should occur for a Remove too.

@amboar
Copy link
Contributor

amboar commented Jun 11, 2025

Yep, that sounds sensible.

@faizana-nvidia
Copy link
Contributor Author

Thank you all for taking out time to look into the PR,

I've addressed to the asked comments on previous commit, added new commit for MCTP Bridge design doc, need to push Polling mechanism now

@jk-ozlabs
Copy link
Member

Thanks for the updates! A couple of comments:

  1. We don't really do design proposal docs as file in the repo; it's great to see your recap of the discussion points from this PR, but there's no need for that format to be long-lived in the repo itself. I would suggest turning this into a user-consumable document describing how things work according to your new implementation. Any dbus API changes belong in the mctpd.md document.

  2. Before implementing this new dbus API, we would need some confirmation that non-contiguous pool allocations are permissible. I have raised an issue with the PMCI WG, (#1540, if you have access), and would like at least some indication that the requirement can be relaxed before we commit to the separate pool ranges.

  3. In order to reduce the upfront work, you may want to skip the endpoint polling for the initial PR; the changes will still be useful in that au.com.codeconstruct.MCTP.Network1.LearnEndpoint can be used to enumerate downstream devices manually (once the pool is allocated, and we can route to those endpoints).

  4. You have a couple of cases where you add something in an initial patch, then re-work it in the follow-up patch. This makes review overly complicated.

  5. Super minor, but the formatting of introduced changes is inconsistent. Given there's still some work to do before this series is ready, I will apply the tree-wide reformat shortly, and add a .clang-format.

@faizana-nvidia faizana-nvidia force-pushed the mctp-bridge-support branch 3 times, most recently from bf8f331 to fa59ed7 Compare June 30, 2025 21:44
@faizana-nvidia
Copy link
Contributor Author

faizana-nvidia commented Jun 30, 2025

Thanks for the updates! A couple of comments:

  1. We don't really do design proposal docs as file in the repo; it's great to see your recap of the discussion points from this PR, but there's no need for that format to be long-lived in the repo itself. I would suggest turning this into a user-consumable document describing how things work according to your new implementation. Any dbus API changes belong in the mctpd.md document.
  2. Before implementing this new dbus API, we would need some confirmation that non-contiguous pool allocations are permissible. I have raised an issue with the PMCI WG, (#1540, if you have access), and would like at least some indication that the requirement can be relaxed before we commit to the separate pool ranges.
  3. In order to reduce the upfront work, you may want to skip the endpoint polling for the initial PR; the changes will still be useful in that au.com.codeconstruct.MCTP.Network1.LearnEndpoint can be used to enumerate downstream devices manually (once the pool is allocated, and we can route to those endpoints).
  4. You have a couple of cases where you add something in an initial patch, then re-work it in the follow-up patch. This makes review overly complicated.
  5. Super minor, but the formatting of introduced changes is inconsistent. Given there's still some work to do before this series is ready, I will apply the tree-wide reformat shortly, and add a .clang-format.

Hello Jeremy

Thank you for looking over the commits, based on your comment # 1 I have removed the new .md file which captured MCTP Bridge support details on PR and updated the existing mctpd.md file with new information about dbus api AssignBridgeStatic. Regarding user consumable document, I'm not much sure what this could be, if you could let me know what this document should be I can create one and update the PR.

I recently got the permission for PMCI WG, have glanced over what was stated on issue #1540, basically the Idea is to split the BusOwner EID pool and segregate a chunk of eids for Bridge's downstream pool on the higher end of Busowner pool while keeping lower end for non-bridge devices. This would be helpful for Dynamic EID assignment of downstream pool devices incase multiple Bridge's are there under same network.

My current implementation involves finding of contiguous eid chunk of min(requested pool size, bridge's pool size capability) from available BusOwner's pool but we begin looking from Asked pool_start (Static) or from next to Bridge's EID (Dynamic) and we look till we get right sized chunk and mark that eid as pool_start. I did based this from the same line of spec for which you raised the issue.

In order to reduce the upfront work, you may want to skip the endpoint polling for the initial PR; the changes will still be useful in that au.com.codeconstruct.MCTP.Network1.LearnEndpoint can be used to enumerate downstream devices manually (once the pool is allocated, and we can route to those endpoints).

I can create a new PR for Endpoint polling if thats what you mean and skip for this PR. Also for adding route for the downstream endpoint to Bridge, we would need your implementation implementation to be merged for both linux kernel and mctpd. Internally I've tested my polling logic with your pulled changes, but for this PR I haven't picked them up so discovery of downstream EID via LearnEndpoint would probably not be possible with only this PR

  1. You have a couple of cases where you add something in an initial patch, then re-work it in the follow-up patch. This makes review overly complicated.
  2. Super minor, but the formatting of introduced changes is inconsistent. Given there's still some work to do before this series is ready, I will apply the tree-wide reformat shortly, and add a .clang-format.

I've updated the patch set now for easier review, hope it helps, let me know if I can do anything else to further ease the review. Thanks for your .clang format, once that is pushed I would reply those onto my change

@faizana-nvidia
Copy link
Contributor Author

Neat! I shall do. I've got a couple of other things pending at the moment, so will get on to a proper review shortly, but in the meantime there are a still a couple of things from the earlier reviews:

  • the dbus API changes need to be documented in mctpd.md, using the per-interface formats there

Ack, let me get onto it.

  • the tests need to cover further cases around invalid endpoint allocation requests, and allocation failures

Thanks, I can incorporate the test case.

conf/mctpd.conf Outdated
Comment on lines 8 to 11
# bus-owner configuration
[bus-owner]
max_pool_size = 15

Copy link
Member

Choose a reason for hiding this comment

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

This splits the uuid configuration example from the section that it belongs in ([mctp]).

I have fixed this up in a couple of cherry-picks in my dev/config branch (which implements configuration for the dynamic EID range, using your initial addition of the [bus-owner] section). Feel free to grab from there if you like, or to rebase on top.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right, overlooked this part, my bad will update

Copy link
Member

Choose a reason for hiding this comment

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

(I have also added some documentation on the configuration in that branch too)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack, reworked my change based on your reference

Copy link
Member

@jk-ozlabs jk-ozlabs left a comment

Choose a reason for hiding this comment

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

Looking good, a few things inline.

Since github doesn't support review comments on the commit messages, just a few general pointers for those. Can you keep consistency with the existing messages? Mainly wrapping at 80 cols, and typically sentence format rather than dot-points.

No need to explain individual code-level changes, we can read that from the diff. Instead, general context and intent is better.

src/mctpd.c Outdated
Comment on lines 1439 to 1440
warnx("%s requested allocation of pool size = %d",
dest_phys_tostr(dest), peer->pool_size);
Copy link
Member

Choose a reason for hiding this comment

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

this isn't really a warning anymore, as it is no longer unimplemented. Just move to a fprintf instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack

src/mctpd.c Outdated
Comment on lines 1730 to 1764
int next_pool_start = get_next_pool_start(
e, n, ctx->max_pool_size);
if (next_pool_start < 0) {
warnx("Ran out of EIDs from net %d while"
"allocating bridge downstream endpoint at %s ",
net, dest_phys_tostr(dest));
is_pool_possible = false;
/*ran out of pool eid : set only bridge eid then
find first available bridge eid which is not part of any pool*/
for (e = eid_alloc_min;
e <= eid_alloc_max; e++) {
if (n->peers[e]) {
// used peer may be a bridge, skip its eid range
e += n->peers[e]
->pool_size;
continue;
}
break;
}
} else if (next_pool_start != e + 1) {
// e doesn't have any contiguous max pool size eids available
e += next_pool_start;
continue;
} else {
// found contigous eids of max_pool_size from bridge_eid
is_pool_possible = true;
}
Copy link
Member

Choose a reason for hiding this comment

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

I would suggest splitting this into a helper function (that calculates the EID and pool range), you're getting pretty deep into the nesting there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay, I'll update this.

src/mctpd.c Outdated
Comment on lines 2252 to 2300
if (peer->pool_size > 0) {
// Call for Allocate EndpointID
}
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer we don't add the stub code until we need it. Or at least mark this as a TODO, as "Call for Allocate EndpointID" doesn't indicate any intent there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

missed to update TODO here. I'll change the comment.

src/mctpd.c Outdated
return -EADDRNOTAVAIL;
}
for (mctp_eid_t e = bridge_eid + 1; e <= bridge_eid + max_pool_size;
e++) {
Copy link
Member

Choose a reason for hiding this comment

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

minor nit: we haven't used the var declaration as part of the loop initialiser style elsewhere at present. I'm not averse to doing so, but in this case it is making your for-loop wrap awkwardly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm I see what you mean. I'll update

assert new
# Assert for assigned bridge endpoint ID
assert path == f'/au/com/codeconstruct/mctp1/networks/1/endpoints/{eid}'
assert new
Copy link
Member

Choose a reason for hiding this comment

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

duplicate assert?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will remove


assert new
# Assert for assigned bridge endpoint ID
assert path == f'/au/com/codeconstruct/mctp1/networks/1/endpoints/{eid}'
Copy link
Member

Choose a reason for hiding this comment

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

no need to assert this, we have checked the path construction elsewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay

#check if the downstream endpoint eid is contiguous to the bridge endpoint eid
assert (eid + i + 1) == br_ep.eid
(path, new) = await net.call_learn_endpoint(br_ep.eid)
assert path == f'/au/com/codeconstruct/mctp1/networks/1/endpoints/{br_ep.eid}'
Copy link
Member

Choose a reason for hiding this comment

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

Same here; no need to check the paths, just that LearnEndpoint succeeded.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack

src/mctpd.c Outdated
rc = sd_bus_message_append(reply, "y", peer->pool_size);
} else if (strcmp(property, "PoolEnd") == 0) {
uint8_t pool_end =
peer->pool_size ?
Copy link
Member

Choose a reason for hiding this comment

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

we know that pool_size > 0, right? otherwise we wouldn't have this interface?

@faizana-nvidia
Copy link
Contributor Author

  • the tests need to cover further cases around invalid endpoint allocation requests, and allocation failures

for this case, we can't really test allocation failure since dbus method AssignEndpoint will at least assign Bridge's own eid, even with case allocated was rejected/failed and we reply bridge's eid path.

Also there are other complications involved while addressing allocation rejection such as

  1. bridge needs to already have some eid, some pool size and downstream eids assigned.
  2. we have not added case where if endpoint have already been assigned any eid should return same as response to SET_ENDPOINT_ID command handling.
  3. coming back to same even all this happen and we get a rejected case, we can't infer that in our test as we have no validation factor in response of dbus method.

for successful allocation we have a factor like assigned eid to downstream endpoints should be within pool range of bridge and contiguous.

I can add invalid endpoint test though i.e if asked EID for some endpoint is falling within any other MCTP bridge pool.

@jk-ozlabs
Copy link
Member

for this case, we can't really test allocation failure since dbus method AssignEndpoint will at least assign Bridge's own eid

That's fine. Just check the resulting pool range of the Allocate Endpoint IDs command, which you would have stored on the Endpoint.

It's fine to leave the more stateful tests (ie, where the bridge already has some EID/pool assignment) for later, but there are definitely some low-hanging fruit to address:

  • where the bridge pool request (in the Set Endpoint ID response) is larger than mctpd's max
  • where the bridge pool request would conflict with some other EID, so there is no valid pool available (use the new dynamic_eid_range support to help trigger that case, and maybe create a static assignment for a conflicting EID)
  • where the endpoint returns a failure from the Assign Endpoint IDs command (is the pool allocation left in a consistent state?)
  • after a successful bridge pool assignment, where a new non-bridge endpoint is enumerated, that it does not conflict with the new pool

@jk-ozlabs
Copy link
Member

You may want to rebase to current main - this means you can drop the max_pool_size change, because it has already been merged.

Add MCTP control message structures for the ALLOCATE_ENDPOINT_ID
command to support bridge endpoint EID pool allocation.

Signed-off-by: Faizan Ali <[email protected]>
@faizana-nvidia faizana-nvidia force-pushed the mctp-bridge-support branch 2 times, most recently from 02e1334 to 6a4c3a1 Compare August 4, 2025 10:48
@faizana-nvidia
Copy link
Contributor Author

faizana-nvidia commented Aug 4, 2025

for this case, we can't really test allocation failure since dbus method AssignEndpoint will at least assign Bridge's own eid

That's fine. Just check the resulting pool range of the Allocate Endpoint IDs command, which you would have stored on the Endpoint.

It's fine to leave the more stateful tests (ie, where the bridge already has some EID/pool assignment) for later, but there are definitely some low-hanging fruit to address:

  • where the bridge pool request (in the Set Endpoint ID response) is larger than mctpd's max
  • where the bridge pool request would conflict with some other EID, so there is no valid pool available (use the new dynamic_eid_range support to help trigger that case, and maybe create a static assignment for a conflicting EID)
  • where the endpoint returns a failure from the Assign Endpoint IDs command (is the pool allocation left in a consistent state?)
  • after a successful bridge pool assignment, where a new non-bridge endpoint is enumerated, that it does not conflict with the new pool

Thanks I've added test cases based on your input and addressed the review comments.

  • where the endpoint returns a failure from the Assign Endpoint IDs command (is the pool allocation left in a consistent state?)

For this use-case we remove the peer altogether so will be the pool. peer would never be setup

@jk-ozlabs
Copy link
Member

  • where the endpoint returns a failure from the Assign Endpoint IDs command (is the pool allocation left in a consistent state?)

For this use-case we remove the peer altogether so will be the pool. peer would never be setup

But you have already sent a Set Endpoint ID, so the peer is set up.

I would think that the peer needs to persist, just with no allocated pool range. You could do the equivalent of a Remove, but then you would have lost connectivity with the bridge. I think it is reasonable for a failed Allocate Endpoint IDs to only fail the pool setup, the bridge can remain.

Regardless, this is still a reasonable scenario to test.

(i have a review in progress, will send that shortly)

@faizana-nvidia
Copy link
Contributor Author

faizana-nvidia commented Aug 5, 2025

  • where the endpoint returns a failure from the Assign Endpoint IDs command (is the pool allocation left in a consistent state?)

For this use-case we remove the peer altogether so will be the pool. peer would never be setup

But you have already sent a Set Endpoint ID, so the peer is set up.

I see, I might have misunderstood you, so ask is about failure in Allocate Endpoint ID control command but Bridge's own eid has been assigned. So that should remain (we are on same page here, changes are dong the same). What I meant earlier was with failure of setup endpoint itself will lead to removal of peer (already part of design)

I would think that the peer needs to persist, just with no allocated pool range. You could do the equivalent of a Remove, but then you would have lost connectivity with the bridge. I think it is reasonable for a failed Allocate Endpoint IDs to only fail the pool setup, the bridge can remain.

Regardless, this is still a reasonable scenario to test.

Need one suggestion here too, since we publish the peer right after successfull Setup endpoint id, so by this time we have one assumed pool size of max_pool_size (toml config), this will create new interface au.com.codeconstruct.MCTP.Bridge1 and later we are resetting the pool (meaning pool size =0 pool start 0) due to failure in Allocate Endpoint ID but this would still mean interface is present, should we manually remove Interface au.com.codeconstruct.MCTP.Bridge1?

(i have a review in progress, will send that shortly)

@jk-ozlabs
Copy link
Member

I see, I might have misunderstood you, so ask is about failure in Allocate Endpoint ID control command

Yes. Sorry, my fault with the ambiguous wording there: should have been "failure from Allocate Endpoint IDs" :)

@faizana-nvidia
Copy link
Contributor Author

I see, I might have misunderstood you, so ask is about failure in Allocate Endpoint ID control command

Yes. Sorry, my fault with the ambiguous wording there: should have been "failure from Allocate Endpoint IDs" :)

Thanks I have update my previous comment with questions, #71 (comment)

@jk-ozlabs
Copy link
Member

Need one suggestion here too, since we publish the peer right after successfull Setup endpoint id, so by this time we have one assumed pool size of max_pool_size (toml config), this will create new interface au.com.codeconstruct.MCTP.Bridge1 and later we are resetting the pool (meaning pool size =0 pool start 0) due to failure in Allocate Endpoint ID but this would still mean interface is present, should we manually remove Interface au.com.codeconstruct.MCTP.Bridge1?

Hm, good question!

I would say that the important thing is that external consumers of the (Bridge1) interface are able to use the interface functionality immediately, and that the data presented is correct.

So, I think that the correct approach would be to publish the Bridge1 interface once the pool assignment is finalised. This would mean waiting until the Allocate Endpoint IDs command has succeeded, and after the gateway range route has been added.

@faizana-nvidia
Copy link
Contributor Author

Need one suggestion here too, since we publish the peer right after successfull Setup endpoint id, so by this time we have one assumed pool size of max_pool_size (toml config), this will create new interface au.com.codeconstruct.MCTP.Bridge1 and later we are resetting the pool (meaning pool size =0 pool start 0) due to failure in Allocate Endpoint ID but this would still mean interface is present, should we manually remove Interface au.com.codeconstruct.MCTP.Bridge1?

Hm, good question!

I would say that the important thing is that external consumers of the (Bridge1) interface are able to use the interface functionality immediately, and that the data presented is correct.

So, I think that the correct approach would be to publish the Bridge1 interface once the pool assignment is finalised. This would mean waiting until the Allocate Endpoint IDs command has succeeded, and after the gateway range route has been added.

Alright, I've updated the logic and added related test case. Thank you !!

@faizana-nvidia
Copy link
Contributor Author

Hello Jeremy,

Thank you taking out time in the reviews, all comments have been addressed, open to any further modifications needed.

@jk-ozlabs
Copy link
Member

OK cool. I'll take a look through soon.

net = await mctpd_mctp_network_obj(dbus, iface.net)
for i in range(pool_size):
br_ep = ep.bridged_eps[i]
assert ep.eid < br_ep.eid <= ep.eid + ep.allocated_pool_size
Copy link
Member

Choose a reason for hiding this comment

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

Like last time, this is just testing the arithmetic in the test suite's own downstream EID assignment.

Just check the allocated start and size outside of this loop, using ep.allocated_pool.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack

@@ -303,6 +303,10 @@ def __init__(self, iface, lladdr, ep_uuid = None, eid = 0, types = None):
self.eid = eid
self.types = types or [0]
self.bridged_eps = []
self.pool_size = 0
self.pool_start = 0
self.allocated_pool_size = 0
Copy link
Member

Choose a reason for hiding this comment

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

These names seem inconsistent; allocated_pool_size is the allocated size, pool_size is our requested pool size, but pool_start is the allocated pool start?

I would suggest

self.allocated_pool = None

Then when we set it in the Allocate Endpoint IDs handler:

self.allocated_pool = (start, size)

So we can determine whether the allocation has ever happened (self.allocated_pool is not None), vs. having the actual values allocated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack

docs/mctpd.md Outdated
Comment on lines 222 to 237
For any endpoint which also happens to be an MCTP Bridge, if dynamic eid is assgined to it via d-bus method `.AssignEndpoint`,
such endpoint's pool allocation details would be reflected into `au.com.codeconstruct.MCTP.Bridge1` interface of bridge's endpoint object.

### `.PoolEnd`: `y`

A constant property representing last EID in the contiguous range allocated for downstream endpoints.

### `.PoolStart`: `y`

A constant property representing first EID in the contiguous range allocated for downstream endpoints.

Copy link
Member

Choose a reason for hiding this comment

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

You have listed these under the spec for the Network1 interface. You'll need a new interface section here - I think just a heading should suffice, using the introduction paragraph you have there.

And please keep the text width consistent with the rest of the file, at 80 cols.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These were added under endpoint object section i.e Endpoint objects: /au/com/codeconstruct/networks//endpoints/

I will add sub heading like BusOwner1 interface introduction for MCTP Interface objects: /au/com/codeconstruct/interfaces/

src/mctpd.c Outdated
Comment on lines 1741 to 1768
if (n->peers[e]) {
// used peer may be a bridge, skip its eid range
e += n->peers[e]->pool_size;
continue;
}

// check for max sized pool from e + 1
if (assign_bridge) {
bool out_of_eids = false;
e = get_bridge_eid(n, ctx, &is_pool_possible,
&out_of_eids, e, net, dest);
if (!is_pool_possible) {
if (!out_of_eids)
continue;
else {
if (e > ctx->dyn_eid_max)
break;
}
}
}

rc = add_peer(ctx, dest, e, net, &peer);
if (rc < 0)
return rc;
if (assign_bridge && is_pool_possible) {
peer->pool_size = ctx->max_pool_size;
peer->pool_start = e + 1;
}
Copy link
Member

Choose a reason for hiding this comment

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

This EID allocation mechanism is way too complex; you have two helper functions, multiple states passed between those, and you have three separate loops over the EID range.

I have a simpler implementation here:

https://github.com/jk-ozlabs/mctp/blob/b3004764ea91913c72d44e62f71a85353ab89523/src/mctpd.c#L1679-L1714

(as part of the commit jk-ozlabs@b300476 )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack, I'll pull in this commit on very top if thats okay?

Copy link
Member

Choose a reason for hiding this comment

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

Feel free to just squash it in to your original change, and/or make any modifications that you see fit.

src/mctpd.c Outdated
bug_warn("%s: Bad old net %d", __func__, netid);
return -EPROTO;
}
if (is_eid_in_bridge_pool(n, ctx, eid)) {
Copy link
Member

Choose a reason for hiding this comment

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

This is creating two separate conflict checks: in add_peer(), we check that the EID is not occupied by a direct peer, and here we check that the EID is not occupied by a bridge range.

Would it make sense to unify these and call a common is_eid_available() from add_peer()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would, I'll update

Copy link
Contributor Author

Choose a reason for hiding this comment

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

on second thought, everytime you add a peer (in future when we poll and see endpoint presence and then create peer for it) you'll have this check while doing add_peer(), but this time downstream eid (for which you are creating peer) will anyways belong to some pool space which would invalidate the function everytime.

So we would then need a way to distinguish:
“Globally free”: EID not already used by a direct peer
“Allowed for this bridge”: EID is inside the correct bridge’s pool (and not inside another bridge’s pool)

Which is why I feel keeping these separate makes sense, plus we only need this to be checked while static eid assignment is involved

Copy link
Member

Choose a reason for hiding this comment

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

perhaps in that case we should ensure that if the EID is allocated as a bridge range, then it is bridged through that specific peer?

There may be other paths that we need to consider. For example, it shouldn't be possible for LearnEndpoint to claim an EID in a bridged range.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There may be other paths that we need to consider. For example, it shouldn't be possible for LearnEndpoint to claim an EID in a bridged range.

LearnEndpoint method under au.com.codeconstruct.MCTP.Network1 interface was purposed to be used for already routed endpoints which in a way could also be used for bridged endpoints as stated in the mctpd.md

We also had relief as polling based downstream endpoint implementation is yet to be added and LearnEndpoints was a way through which downstream endpoints could be exposed too under d-bus.

but I see in current implementation we don't really have a way to check if there is a route to that endpoint for which we have called LearnEndpoint other than checking peer itself is present in the maintained structure for that eid via find_peer_by_addr method. May be we should integrate all this checks for eid being part of bridge to this method itself?

perhaps in that case we should ensure that if the EID is allocated as a bridge range, then it is bridged through that specific peer?

do you mean don't let any external methods to engage in taking up that eid and only polling mechanism is a way to expose endpoint on dbus? if so then we need that check right which I added in AssignEndpointStatic method?

Copy link
Member

Choose a reason for hiding this comment

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

LearnEndpoint method under au.com.codeconstruct.MCTP.Network1 interface was purposed to be used for already routed endpoints which in a way could also be used for bridged endpoints as stated in the mctpd.md

I was more referring to BusOwner1.LearnEndpoint here, which is another facility (alongside LearnEndpointStatic) which may consume an eid without the bridge pool checking, as it calls add_peer() directly.

Ideally, we would want to check all EID allocations via add_peer(). That would involve allowing EID assignments that have addresses that we "exepect": if they're routed behind some other peer that we know about, it should use an EID in that pool's range. If not, we should reject the assignment.

As you say though, we may be missing some information to properly do that. We can defer this checking to later if necessary.

However, In the case of BusOwner1.LearnEndpoint though, we know that this is a directly-connected peer (we are addressing it physically), so should probably disallow any learnt EID in a pool range in that path too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was more referring to BusOwner1.LearnEndpoint here, which is another facility (alongside LearnEndpointStatic) which may consume an eid without the bridge pool checking, as it calls add_peer() directly.

So all other usecase such as AssignStaticEid SetupEndpoint LearnEndpoint(BusOwner if) requires this check i.e if eid under discussion belongs to some peer pool space then reject the assignment. But on contrary LearnEndpoint (Network if) and in future Bridge Polling would actually want the EID to be part of already available routes or part of some bridge's pool space.

LearnEndpoint (Network if) was implemented under assumption that MCTP network would have direct-connected pee only, but we might need to refine it more due to introduction of Bridge support.

As a proposal I think we need to implement a utility method under mctp-netlink.c to fetch out this information about eid is routed to some interface/bridge eid or not, for full LearnEndpoint method functionality, something I could pick for future PR. (open to any other suggestions to handle this)

so for now I've made the check within add_peer() to reject the assignment if eid falls within any of pool space.

Copy link
Member

Choose a reason for hiding this comment

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

So all other usecase such as AssignStaticEid SetupEndpoint LearnEndpoint(BusOwner if) requires this check i.e if eid under discussion belongs to some peer pool space then reject the assignment. But on contrary LearnEndpoint (Network if) and in future Bridge Polling would actually want the EID to be part of already available routes or part of some bridge's pool space.

Yes, exactly.

LearnEndpoint (Network if) was implemented under assumption that MCTP network would have direct-connected pee only, but we might need to refine it more due to introduction of Bridge support.

No, it was implemented under the assumption that there is some existing route to the device, and it doesn't matter whether directly-connected or via a gateway. The primary use-case for this is for bridged endpoints though.

As a proposal I think we need to implement a utility method under mctp-netlink.c to fetch out this information

We have some information on the bridge structure, but only for bridges that are managed by mctpd. You're correct - we would need the netlink data for other (non-mctp-managed) bridge routes.

(A valid design here may be that we only support bridging through mctp-managed bridges)

So, there's a bit of complexity here.

so for now I've made the check within add_peer() to reject the assignment if eid falls within any of pool space.

This may break an intended usage of Network1.LearnEndpoint, no? where:

  • a bridge is managed by mctpd
  • a user calls Network1.LearnEndpoint on a peer behind that bridge

ie., this case is required for scenarios where the bridged endpoints are not automatically polled by mctpd.

In general: I am fine with doing a partial implementation now - we don't need to have all scenarios covered at the moment. We do need to make sure we don't "paint ourselves into a corner" though, where a future change would break existing supported usage.

if (rc) {
warnx("Failed to emit add %s signal for endpoint %d : %s",
CC_MCTP_DBUS_IFACE_BRIDGE, peer->eid,
strerror(-rc));
Copy link
Member

Choose a reason for hiding this comment

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

I am hitting this warning running tests:

$ (cd obj && pytest -k test_assign_dynamic_bridge_eid --capture=no)
======================================================= test session starts ========================================================
platform linux -- Python 3.11.2, pytest-8.3.2, pluggy-1.6.0
rootdir: /home/jk/devel/mctp/mctp/obj
configfile: pytest.ini
testpaths: /home/jk/devel/mctp/mctp/tests
plugins: anyio-4.9.0, trio-0.8.0, tap-3.5
collected 46 items / 45 deselected / 1 selected                                                                                    

test_mctpd.py Adding interface #1 mctp0
net 1 added, path /au/com/codeconstruct/mctp1/networks/1
test-mctpd: emitting net add: /au/com/codeconstruct/mctp1/networks/1
Adding local eid 8 net 1
test-mctpd: emitting endpoint add: /au/com/codeconstruct/mctp1/networks/1/endpoints/8
test-mctpd: emitting interface add: /au/com/codeconstruct/mctp1/interfaces/mctp0
linkmap
   1: mctp0, net 1 up local addrs [8]
test-mctpd: read_message got from sockaddr_mctp_ext eid 0 net 1 type 0x00 if 1 hw len 1 0x1d len 6
physaddr if 1 hw len 1 0x1d requested allocation of pool size = 2
Adding neigh to peer eid 9 net 1 phys physaddr if 1 hw len 1 0x1d state 0
Adding route to peer eid 9 net 1 phys physaddr if 1 hw len 1 0x1d state 0
test-mctpd: read_message got from sockaddr_mctp_ext eid 9 net 1 type 0x00 if 1 hw len 1 0x1d len 6
test-mctpd: read_message got from sockaddr_mctp_ext eid 9 net 1 type 0x00 if 1 hw len 1 0x1d len 19
Adding neigh to peer eid 9 net 1 phys physaddr if 1 hw len 1 0x1d state 0
short error message (4 bytes)
Adding route to peer eid 9 net 1 phys physaddr if 1 hw len 1 0x1d state 0
test-mctpd: emitting endpoint add: /au/com/codeconstruct/mctp1/networks/1/endpoints/9
test-mctpd: read_message got from sockaddr_mctp_ext eid 9 net 1 type 0x00 if 1 hw len 1 0x1d len 6
Allocation accepted
Allocated size of 2, starting from EID 10
test-mctpd: Failed to emit add au.com.codeconstruct.MCTP.Bridge1 signal for endpoint 9 : Unknown error -1
Downstream EIDs assigned from 10 to 11 : pool size 2
test-mctpd: read_message got from sockaddr_mctp_ext eid 10 net 1 type 0x00 if 1 hw len 0 0x len 6
test-mctpd: read_message got from sockaddr_mctp_ext eid 10 net 1 type 0x00 if 1 hw len 0 0x len 19
test-mctpd: emitting endpoint add: /au/com/codeconstruct/mctp1/networks/1/endpoints/10
test-mctpd: endpoint_query_addr: receive timed out from sockaddr_mctp_ext eid 11 net 1 type 0x00 if 0 hw len 0 0x
test-mctpd: Error getting endpoint types for peer eid 11 net 1 phys physaddr if 0 hw len 0 0x state 0. Ignoring error -110 Connection timed out
test-mctpd: endpoint_query_addr: receive timed out from sockaddr_mctp_ext eid 11 net 1 type 0x00 if 0 hw len 0 0x
test-mctpd: Error getting UUID for peer eid 11 net 1 phys physaddr if 0 hw len 0 0x state 0. Ignoring error -110 Connection timed out
test-mctpd: emitting endpoint add: /au/com/codeconstruct/mctp1/networks/1/endpoints/11
.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack : if check needs to change (on success positive integer is expected) if (rc < 0)

# Interface should not be present for failed pool allocation
with pytest.raises(asyncdbus.errors.InterfaceNotFoundError):
bridge_obj = await dbus.get_proxy_object(MCTPD_C, path)
await bridge_obj.get_interface(MCTPD_ENDPOINT_BRIDGE_I)
Copy link
Member

Choose a reason for hiding this comment

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

Invalid whitespace here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ack removed

Add support for MCTP bridge endpoints that can allocate pools of EIDs
for downstream endpoints.

We assume each AssignEndpoint d-bus call will be for an MCTP bridge,
with this we allocate/reserve a max_pool_size eid range contiguous to
bridge's own eid. Later this pool size is updated based on
SET_ENDPOINT_ID command response.

- for static eid assignment via AssignEndpointStatic d-bus call,
add check if eid is part of any other bridge's pool range.

[Fixup and requested change from Jeremy Kerr <[email protected]>]

Signed-off-by: Faizan Ali <[email protected]>
Signed-off-by: Jeremy Kerr <[email protected]>
Add implementation for the MCTP ALLOCATE_ENDPOINT_ID control command
to enable bridges to allocate EID pools for downstream endpoints.

Update gateway route for downstream EIDs

Signed-off-by: Faizan Ali <[email protected]>
* updated mctpd.md with new mctp bridge support for dynamic eid
assignment from AssignEndpoint d-bus call

Signed-off-by: Faizan Ali <[email protected]>
Add new test for validating AssignEndpoint D-Bus method
to verify bridge endpoint EID allocation being contiguous to its
downstream eids. Add Allocate Endpoint control message support with
new endpoint property for allocated pool size also assign dynamic eid
contiguous to bridge during Allocate Endpoint control message.

Signed-off-by: Faizan Ali <[email protected]>
New endpoint object interface au.com.codeconstruct.MCTP.Bridge1
which will capture details of bridge type endpoint such as
pool start, pool end.

Update test framework with new test methods to validate bridge
pool assignemnt.

Signed-off-by: Faizan Ali <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants