-
Notifications
You must be signed in to change notification settings - Fork 46
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
Add bonding support #885
Add bonding support #885
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! It is a lot of work.
I have not reviewed the code 100%, but it goes in the right direction. I have some suggestions (do not fear combinators 😉) but we can discuss them in a pair programming session.
Action::UpdateControllerConnection(conn, settings, tx) => { | ||
let id = &conn.clone(); | ||
let id = id.id(); | ||
self.state.update_controller_connection(conn, settings)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method could return a reference to the updated connection.
/// Additionally, it registers the connection to be removed when the changes are applied. | ||
pub fn update_controller_connection( | ||
&mut self, | ||
mut conn: Connection, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing docs
e8c9872
to
aeed3ab
Compare
let opts = &self | ||
.0 | ||
.iter() | ||
.map(|(key, value)| format!("{key}={value}")) | ||
.collect::<Vec<_>>(); | ||
|
||
write!(f, "{}", opts.join(" ")) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not enough rusty?
let opts = &self | |
.0 | |
.iter() | |
.map(|(key, value)| format!("{key}={value}")) | |
.collect::<Vec<_>>(); | |
write!(f, "{}", opts.join(" ")) | |
} | |
let mut spacer = ""; | |
for (key, value) in &self.0 { | |
write!(f, "{spacer}{key}={value}")?; | |
spacer = " "; | |
} | |
Ok(()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I would say that it is more a Ruby-like approach :-) Both of them look expressive enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the later would have less implicit memory effort, as it writes directly to the stream. But there is not critical path and I don't have hard feelings here :-)
ConnectionBuilder::new(port) | ||
.with_type(DeviceType::Ethernet) | ||
.with_interface(port) | ||
.with_controller(controller.id()) | ||
.build() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm struggle with this, when I think of a scenario which have a other DeviceType then Ethernet, e.g.:
wlan0 -m-+
|-> bond0
eth2 -m-+
How do you think we are going to implement this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should obtain the device type from the system, so this is actually an issue that we might fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is, if it's also a virtual interface like:
vlan9 -m-+
|-> bond0
eth2 -m-+
And we just about to create them?
|
||
let path = if let Ok(proxy) = self.get_connection_proxy(conn.uuid()).await { | ||
let original = proxy.get_settings().await?; | ||
let merged = merge_dbus_connections(&original, &dbus_conn); | ||
proxy.update(merged).await?; | ||
OwnedObjectPath::from(proxy.path().to_owned()) | ||
} else { | ||
let proxy = SettingsProxy::new(&self.connection).await?; | ||
proxy.add_connection(dbus_conn).await? | ||
}; | ||
|
||
if let Connection::Bond(bond) = conn { | ||
let _ = bond.bond.ports.iter().map(|port| { | ||
self.add_or_update_port_connection( | ||
port, | ||
bond.base.interface.to_string(), | ||
"bond".to_string(), | ||
) | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be the same as as simply calling add_or_update_connection()
?
I see, there are small differences, e.g. not calling cleanup_dbus_connection()
, but why?
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I miss something, but for me it looks like we are putting all connections which do not have a controller
(none port connections) into a vector which is in that hashmap controllers
... the other connections into connections
. But we forget to link them. Which I think is mandatory, or we would loose the ports config...
"ports": { | ||
"type": "array", | ||
"items": { | ||
"description": "A list of the ports to be bonded", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we need to be more precise here:
- Is it a connection name or interface name
- What happen if it is a connection name, but no connection with this name exists (defaults)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not convinced to store the ports
inside the controller device.
Of course it is nice in the first place, when we have a simple configuration where we just wanna add some ports to a bond. But if theses ports
it self are something different (wireless, vlan, bridge) they need a connection definition anyway.
If we do not have a connection of a port
and we are going to auto create one, I think we should do it next to the user input. And internally just work with a list of connections
, which have only one link to a lower/parent device (like VLAN has) and one link to a upper/controller (like a bridge/bond ports have).
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] | ||
pub enum BondMode { | ||
#[serde(rename = "balance-rr")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a hint. As I'm not sure what makes the more sense/ is better to read!
In our xml parsing part we used serde_with and strum_macros crates to achieve similar things.
https://github.com/jcronenberg/agama/blob/523ebe9abf4055c67952d2d50e6f1fbe734044b5/rust/agama-migrate-wicked/src/interface.rs#L167
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have been considering introducing strum_macros
for some time, but we were unsure about introducing another dependency. But, to be honest, we already have a good share of them :-). So perhaps we follow the same path as you. Thanks!
1e3466d
to
2052bf6
Compare
* Just for consistency reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just had a quick look over mostly just the docs. Sorry for being nitpicky with the grammar and documentation 🙂
@@ -43,27 +45,50 @@ impl NetworkState { | |||
|
|||
/// Get connection by UUID | |||
/// | |||
/// * `uuid`: connection UUID | |||
/// * `id`: connection UUID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// * `id`: connection UUID | |
/// * `uuid`: connection UUID |
Co-authored-by: Jorik Cronenberg <jcronenberg@suse.de>
for (k, v) in wireless_dbus { | ||
result.insert(k, v); | ||
match &conn { | ||
Connection::Ethernet(_) | Connection::Loopback(_) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think is_ethernet()
was the hook to identify connections which need to set the customized macaddr via 802-3-ethernet.assigned-mac-address
. AFAIK Dummy was one of them and Bond should be one.
If we do it this way. We would need to set mac in the corresponding match block of Connection::Bond
and Connection::Dummy
.
Or we extract the writing of 802-3-ethernet.assigned-mac-address
out of this match block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, thx for the effort!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It works in our tests 🎆 (creating and updating the bond and its ports).
## Problem Agama's network model is heavily inspired by [nmstate](https://github.com/nmstate/nmstate/blob/c9fd1e80d3c87e1f844edcd86e9d36ae499ce717/rust/src/lib/ifaces/ethernet.rs#L64). Connections are represented [by an enum](https://github.com/openSUSE/agama/blob/2bc452b39e4460122ab52c5a72ee0d1dfe154cec/rust/agama-dbus-server/src/network/model.rs#L220) and each variant contains a struct which depends on the type (e.g., the [WirelessConnection](https://github.com/openSUSE/agama/blob/2bc452b39e4460122ab52c5a72ee0d1dfe154cec/rust/agama-dbus-server/src/network/model.rs#L542) struct). Those structs have a `base` field that points to a [BaseConnection struct](https://github.com/openSUSE/agama/blob/master/rust/agama-dbus-server/src/network/model.rs#L328) which holds the common fields for all connection types. And that's perfectly fine. However, we decided to introduce some methods to hide the fact that the common information lives in a separate `struct`. We could say it is for encapsulation reasons, but I don't know whether it is a good idea. Even we created some *getters* and *setters* for those fields, adding quite some [boilerplate](https://github.com/openSUSE/agama/blob/master/rust/agama-dbus-server/src/network/model.rs#L264-L299) [code](https://github.com/openSUSE/agama/blob/master/rust/agama-dbus-server/src/network/model.rs#L318-L324) Additionally, until we introduced the `ConnectionBuilder` (in #885), creating a new connection with some custom data (like an id and a UUID) was not straightforward because you needed to make the `BaseConnection` first (see [this example](https://github.com/openSUSE/agama/blob/2bc452b39e4460122ab52c5a72ee0d1dfe154cec/rust/agama-dbus-server/src/network/model.rs#L136-L141). ## Proposal Although I am not 100% sure whether it is a better idea, I am exploring the possibility of using a simpler layout to represent a connection. In this case, we would use a single `struct` containing the common fields plus a `config` field for specific stuff. That field contains an `enum` which represents each kind of connection. ```rust /// Represents an availble network connection #[derive(Debug, Default, Clone)] pub struct Connection { pub id: String, pub uuid: Uuid, pub mac_address: MacAddress, pub ip_config: IpConfig, pub status: Status, pub interface: String, pub match_config: MatchConfig, pub config: ConnectionConfig, } #[derive(Default, Debug, PartialEq, Clone)] pub enum ConnectionConfig { #[default] Ethernet, Wireless(WirelessConfig), Loopback, Dummy, } ``` And not getters/setters as they are not needed at all. ## Testing - Adapted the unit tested.
## Problem Until now, the communication between our network model and its D-Bus interface worked only in one direction (from D-Bus to the model). And that was pretty limiting. ## Solution In #885 we introduce the usage of `oneshot::channel()` to enable bi-directional communication between the model and the D-Bus interface. It worked rather well, so it is time to extend this approach. Some use cases that are covered: * Perform a complex validation in the model (considering the whole model) and return the result to the D-Bus interface. * Return the D-Bus path of a new connection (implemented in this PR). But there is another interesting implication: the model is the single source of truth (no more cloned data in the interface). In the future, we are expected to listen to NetworkManager and update some parts of our model accordingly. Not having to replicate that information to D-Bus (except when new things add or disappear) makes things easier. Note: `org.opensuse.Agama1.Network.Device` and `org.opensuse.Agama1.Network.Devices` are excluded by now. ## Testing - *Added a new unit test* - *Tested manually* ## To do - [x] Reduce the boilerplate associated to the usage of channels.
This PR adds support for configuring a Bond connection using Agama auto-installation.
The model
Initially, we thought each connection struct would take ownership of its ports (basically, other connections). However, after some discussion with the networking team, we agreed to keep a "reference" in the opposite direction (from the port to the controller). At this point, each connection that belongs to a bond keeps its parent UUID.
Configuring a port
When using the auto-installation you can define the port with something like this:
In NetworkManager, it is mandatory to specify an interface name. However, Agama will use the
id
if you do not define theinterface
. About the ports, it is not required to define them (in the example above,eth0
is missing). Agama will create a connection automatically. And, again, you can use theid
or theinterface
to link both connections.Improving the D-Bus interface
As the list of ports is not part of the
BondConnection
struct, it is not straightforward to expose that information in the D-Bus interface. After all, the code that exposes each connection over D-Bus only had access to the information of its specific connection (actually, it owns a clone of the connection).For that reason, starting with the Bond interface, we implemented a better mechanism (based on message passing) to interact with the data model. We will open a separate PR to adapt the rest of interfaces (with more information about the problems it solves).
What's next?
References
Testing