Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add bonding support #885

Merged
merged 61 commits into from
Dec 15, 2023
Merged

Add bonding support #885

merged 61 commits into from
Dec 15, 2023

Conversation

teclator
Copy link
Contributor

@teclator teclator commented Nov 22, 2023

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:

      {
        id: 'bond0',
        bond: {
            ports: ['eth0', 'eth1'],
            mode: 'active-backup',
            options: "primary=eth1"

        }
      },
      {
        id: 'Wired Adapter 1',
        interface: 'eth1'
      }

In NetworkManager, it is mandatory to specify an interface name. However, Agama will use the id if you do not define the interface. 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 the id or the interface 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

@coveralls
Copy link

coveralls commented Nov 22, 2023

Coverage Status

coverage: 74.655% (-0.4%) from 75.051%
when pulling 9edb940 on add_bonding_support
into c374ec9 on master.

Copy link
Contributor

@imobachgs imobachgs left a 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.

rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
Action::UpdateControllerConnection(conn, settings, tx) => {
let id = &conn.clone();
let id = id.id();
self.state.update_controller_connection(conn, settings)?;
Copy link
Contributor

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,
Copy link
Contributor

Choose a reason for hiding this comment

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

missing docs

Comment on lines +632 to +629
let opts = &self
.0
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<_>>();

write!(f, "{}", opts.join(" "))
}
Copy link
Contributor

@cfconrad cfconrad Nov 28, 2023

Choose a reason for hiding this comment

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

Not enough rusty?

Suggested change
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(())

Copy link
Contributor

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.

Copy link
Contributor

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 :-)

Comment on lines 106 to 110
ConnectionBuilder::new(port)
.with_type(DeviceType::Ethernet)
.with_interface(port)
.with_controller(controller.id())
.build()
Copy link
Contributor

@cfconrad cfconrad Nov 28, 2023

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?

Copy link
Contributor Author

@teclator teclator Nov 30, 2023

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

Copy link
Contributor

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?

Comment on lines 147 to 166

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(),
)
});
}
Copy link
Contributor

@cfconrad cfconrad Nov 28, 2023

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?

}
}

Copy link
Contributor

@cfconrad cfconrad Nov 28, 2023

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",
Copy link
Contributor

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:

  1. Is it a connection name or interface name
  2. What happen if it is a connection name, but no connection with this name exists (defaults)

Copy link
Contributor

@cfconrad cfconrad left a 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).

Comment on lines +42 to +44
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
pub enum BondMode {
#[serde(rename = "balance-rr")]
Copy link
Contributor

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

Copy link
Contributor

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!

@imobachgs imobachgs marked this pull request as ready for review December 13, 2023 22:37
Copy link
Contributor

@jcronenberg jcronenberg left a 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 🙂

rust/agama-dbus-server/src/network/action.rs Outdated Show resolved Hide resolved
@@ -43,27 +45,50 @@ impl NetworkState {

/// Get connection by UUID
///
/// * `uuid`: connection UUID
/// * `id`: connection UUID
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// * `id`: connection UUID
/// * `uuid`: connection UUID

rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
rust/agama-lib/src/network/client.rs Outdated Show resolved Hide resolved
rust/agama-lib/src/network/client.rs Outdated Show resolved Hide resolved
rust/agama-lib/src/network/proxies.rs Outdated Show resolved Hide resolved
rust/agama-lib/src/network/proxies.rs Outdated Show resolved Hide resolved
rust/agama-dbus-server/src/network/model.rs Outdated Show resolved Hide resolved
Co-authored-by: Jorik Cronenberg <jcronenberg@suse.de>
for (k, v) in wireless_dbus {
result.insert(k, v);
match &conn {
Connection::Ethernet(_) | Connection::Loopback(_) => {
Copy link
Contributor

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.

Copy link
Contributor

@cfconrad cfconrad left a 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!

Copy link
Contributor

@imobachgs imobachgs left a 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).

@teclator teclator merged commit 627f657 into master Dec 15, 2023
2 checks passed
@teclator teclator deleted the add_bonding_support branch December 15, 2023 11:30
imobachgs added a commit that referenced this pull request Dec 18, 2023
## 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.
@imobachgs imobachgs mentioned this pull request Dec 26, 2023
1 task
imobachgs added a commit that referenced this pull request Dec 27, 2023
## 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.
@imobachgs imobachgs mentioned this pull request Feb 12, 2024
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