Skip to content

Introduce support for networkd address options#89

Merged
slyon merged 2 commits intocanonical:masterfrom
hrasiq:master
Aug 4, 2020
Merged

Introduce support for networkd address options#89
slyon merged 2 commits intocanonical:masterfrom
hrasiq:master

Conversation

@hrasiq
Copy link
Contributor

@hrasiq hrasiq commented Jun 7, 2019

Description

This patch introduces a way to set options for the [Address] section in the networkd backend. It changes the addresses YAML key to a sequence of scalars and mappings, keeping backward compatibility with the previous address notation.

We can extend the current address notation either flattened or expanded, e.g. below:

addresses: [192.168.14.2/24, "2001:1::1/64": {lifetime: 0}]
addresses:
  - 192.168.14.2/24:
      lifetime: 0
      label: test-label
  - 2001:FFfe::1/64''')

Due to the nature of the networkd backend, addresses which have these options set need to be placed in their separate [Address] section in the resulting .network file.

Two keys are supported for now: lifetime and label, which correspond to their networkd counterparts PreferredLifetime and Label. I've tried to write this in a way that makes it easy to add other keys in the future, but I'm open to other different approaches in changing the code.

Checklist

  • Runs make check successfully.
  • Retains 100% code coverage (make check-coverage).
  • New/changed keys in YAML format are documented.
  • (Optional) Closes an open bug in Launchpad. LP: #1803203

@codecov-io
Copy link

codecov-io commented Jun 7, 2019

Codecov Report

Merging #89 into master will not change coverage.
The diff coverage is 100%.

Impacted file tree graph

@@          Coverage Diff           @@
##           master    #89    +/-   ##
======================================
  Coverage     100%   100%            
======================================
  Files          34     36     +2     
  Lines        4066   4912   +846     
======================================
+ Hits         4066   4912   +846
Impacted Files Coverage Δ
tests/generator/test_tunnels.py 100% <ø> (ø) ⬆️
tests/generator/test_vlans.py 100% <ø> (ø) ⬆️
tests/generator/test_bridges.py 100% <ø> (ø) ⬆️
tests/generator/test_routing.py 100% <ø> (ø) ⬆️
tests/generator/base.py 100% <ø> (ø) ⬆️
tests/generator/test_errors.py 100% <ø> (ø) ⬆️
tests/generator/test_args.py 100% <ø> (ø) ⬆️
src/nm.c 100% <100%> (ø) ⬆️
tests/generator/test_bonds.py 100% <100%> (ø) ⬆️
netplan/cli/commands/__init__.py 100% <100%> (ø) ⬆️
... and 16 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 66edcfa...08424e6. Read the comment docs.

@vorlonofportland
Copy link

From a design perspective, what is the rationale for supporting either of these in netplan?

'Label' would appear to only be useful for tools which are managing networkd state directly and bypassing netplan. Do you have a practical example of this, and of why you would want the addresses to be initially configured via netplan but then reconfigured via networkd?

PreferredLifetime takes only two values: "forever", and "0", where 0 means the address will not be used by default and only used if an application explicitly requests it.

That's an interesting idea, can you speak to the practical need for it?

In terms of the schema, I'm not in love with the idea of having addresses as both scalars and mappings, but I don't immediately see a cleaner way to do this.

@hrasiq
Copy link
Contributor Author

hrasiq commented Jun 12, 2019

From a design perspective, what is the rationale for supporting either of
these in netplan?

One of the main points in favor of supporting these in netplan is to have
feature parity to the systemd backend. If systemd-networkd (or networkmanager)
has features that can't be described in netplan, I think that it kind of defeats
the point of having the unified configuration YAML (users that need those
features will just use the backend directly and skip netplan).

PreferredLifetime takes only two values: "forever", and "0", where 0 means the
address will not be used by default and only used if an application explicitly
requests it. That's an interesting idea, can you speak to the practical need
for it?

Setting the lifetime to "0" allows one to have multiple IP addresses where each
one serves a specific purpose, while still keeping outbound traffic "tied" to
one address. If e.g. we have a server listening on multiple addresses, we
might want to have outbound traffic always from one specific source address (a
"primary" address of sorts). This can be achieved by deprecating the other
addresses that are not the "primary" outbound one, and one way of doing this is
by setting other lifetimes to zero. For a more practical example, this scenario
would appear in a multi-VM deployment where having more than one IP address per
host is common.
We could achieve a similar effect with clever routing rules, but those might
need to be updated more frequently depending on specifics for each
deployment. As the lifetime feature already exists in systemd-networkd, I think
it makes sense to have that alternative in netplan as well.

I've added the label key with this kind of scenario in mind, as it ties in
nicely with "organizing" multiple addresses. Ultimately, I think it would be
nice to have all the keys from [0] available in netplan too, but I've started
the patch focusing on the lifetime one due to Ubuntu users running into that
issue (LP: #1803203).

In terms of the schema, I'm not in love with the idea of having addresses as
both scalars and mappings, but I don't immediately see a cleaner way to do
this.

I agree that mixing types is not the cleanest solution, but I'm also having a
hard time coming up with a better one that doesn't break the previous YAML
notation. LP#1803203 has some more details on different approaches, but
I think this one has less impact on the schema. One of the objectives
with this PR was to also discuss the schema changes, so if we get to an
alternative that makes more sense than the one I went with at first, that would
be great too :)

[0] https://www.freedesktop.org/software/systemd/man/systemd.network.html#%5BAddress%5D%20Section%20Options

@hrasiq
Copy link
Contributor Author

hrasiq commented Feb 5, 2020

It has been a while since any activity on this PR, so I've updated it to be aligned with the current master branch.

Is there anything else that I can do to help with the review? I would be happy to implement any changes or suggestions if there are any problems with the current schema changes. I think the lifetime parameter is the interesting one in this case, so we can leave other additional changes for a separate time if needed.

@jfesler
Copy link

jfesler commented Jun 14, 2020

I for one could use something like this. I have a need for multiple incoming addresses, but wish to control which single outbound address is selected. Particularly since some addresses I have network controls in place.

@codecov-commenter
Copy link

codecov-commenter commented Jun 29, 2020

Codecov Report

Merging #89 into master will decrease coverage by 0.25%.
The diff coverage is 82.66%.

Impacted file tree graph

@@             Coverage Diff             @@
##            master      #89      +/-   ##
===========================================
- Coverage   100.00%   99.74%   -0.26%     
===========================================
  Files           41       41              
  Lines         5082     5157      +75     
===========================================
+ Hits          5082     5144      +62     
- Misses           0       13      +13     
Impacted Files Coverage Δ
src/parse.c 98.46% <66.66%> (-1.54%) ⬇️
src/networkd.c 100.00% <100.00%> (ø)
src/nm.c 100.00% <100.00%> (ø)
tests/generator/test_common.py 100.00% <100.00%> (ø)
tests/generator/test_errors.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 50ac1a1...bafa8ce. Read the comment docs.

@slyon
Copy link
Contributor

slyon commented Jun 30, 2020

This would also resolve LP #1743200 (https://bugs.launchpad.net/ubuntu/+source/nplan/+bug/1743200)

Did you do any research of how this could be implemented for the NetworkManager backend? Ideally, we should implement this feature for both backends equally. If it's not possible, we should at least throw an error from src/nm.c and exit with a message that this feature is not supported for the NetworkManager backend, if the feature is used (e.g. def->address_options is set).

@hrasiq
Copy link
Contributor Author

hrasiq commented Jun 30, 2020

@slyon As far as I've seen from the nm-settings documentation [0], it doesn't
have support for lifetimes currently. The closest would be the never-default
setting, but that would only make sense for a lifetime value of 0 and would
probably cause confusion if we tried linking it with the networkd lifetime
somehow. It seems there's also no support for labels in the NM documentation, so
I've also skipped that in my initial patch.

I do agree that we should try to handle this gracefully in both backends
however, so I'll see if I can implement some safeguards in the NM backend for
this change and resubmit the patch for revision. Thank you for bringing this up!

[0] https://developer.gnome.org/NetworkManager/stable/nm-settings.html

@slyon
Copy link
Contributor

slyon commented Jul 2, 2020

Yes, I couldn't find anything related to lifetimes/labels in NM documentation either. So it would be fine if you implement a simple safeguard in src/nm.c similar to this one, but checking for def->address_options (or maybe its length) instead:

    if (def->ipv6_mtubytes) {
        g_fprintf(stderr, "ERROR: %s: NetworkManager definitions do not support ipv6-mtu\n", def->id);
        exit(1);
    }

Reference: https://github.com/CanonicalLtd/netplan/blob/master/src/nm.c#L543

@slyon slyon self-requested a review July 16, 2020 16:17
Copy link
Contributor

@slyon slyon left a comment

Choose a reason for hiding this comment

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

Thank you @hrasiq for your continued effort of getting this PR ready for merging.
Your work is very much appreciated!

The safeguard you implemented for the NetworkManager backend is looking good and (unfortunately) seems the best we can do at the moment, as NM does not support this feature.

So here comes your full technical code review!
I like the overall design and structure of your PR, but requested some smaller cosmetic or technical improvements inline. Especially the renaming of addr_options -> NetplanAddressOptions, as this is now exported via libnetplan.

The only bigger issue I could spot is that the address options data might get appended multiple times to the cur_netdef->address_options array in handle_addresses. This is a similar problem to what I fixed here recently: cc013bd Please have a look there for reference and lets discuss the solution here, if need be.

I'll try to get the other team members to sign-off on the schema changes for this PR!

Thanks!
Lukas

if (!process_mapping(doc, value, address_option_handlers, error))
return FALSE;

g_array_append_val(cur_netdef->address_options, cur_addr_option);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a bit tricky... But you should make sure not to add the same options data multiple times into the array. This can happen as this function might be called multiple times, if the YAML parser needs multiple passes to resolve all netplan interfaces.

I implemented this similarly in this commit recently: cc013bd
Please have a look there for reference and let's discuss in this PR how to best solve it for the NetplanAddressOptions data.

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've thought a bit about this, and wanted to get more input before "tainting" this PR with more complex code (maybe I'll push a separate commit on top of this one?).

One "easy" way to do this would be to check if the first element on the cur_netdef->address_options array is equal to the struct we're currently pushing there (meaning we're past the first YAML parsing round). This would probably need an additional comparison helper, and we would also need to take into account addresses that are set without specifying any of the options (these would skip this code block, and would need to be handled further down in their IPv4/IPv6 sections).

How is this being handled currently for addresses without any options? How viable would it be to try and detect when the parser needs multiple passes, so that we can just verify if the addresses were set correctly and completely skip the helpers here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Pushing a separate commit on top is a good idea. Keep it in small logical chunks/commits. We will squash all commits from this PR into a single "merge commit" anyways, once it is accepted into master.

It might be even easier to compare the length cur_netdef->address_options->len. It will be 0 on first pass, while it will be equal to the length of the YAML sequence in following passes. This is what I did in the other commit (cc013bd):

    /* Avoid adding the same peers in a 2nd parsing pass by comparing
       the array size to the YAML sequence size. Skip if they are equal. */
    guint item_count = node->data.sequence.items.top - node->data.sequence.items.start;
    if (cur_netdef->wireguard_peers->len == item_count) {
        g_debug("%s: all wireguard peers have already been added", cur_netdef->id);
        return TRUE;
    }

For IP4/IP6 addresses this is currently handled by comparing all IP address strings of the array, which might be a bit overkill... as each parsing pass will contain the very same IP address sequence. And in fact this fix did not yet make it to the master branch, but can be seen here: 2f2971b (It was introduced as part of a OVS pull request which are currently still tracked in the openvswitch-support branch.

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'm not sure the length comparison will work out in this case. If we take the
example below, for instance:

network:
  version: 2
  ethernets:
    engreen:
      addresses:
        - 192.168.14.2/24:
            label: out
            lifetime: 0
        - 192.168.14.3/24
        - 2001:FFfe::1/64:
            label: ipv6

For this example, node->data.sequence.items.top - node->data.sequence.items.start will result in 3 (two IPv4 keys and one IPv6 key), but we'll never reach that amount in cur_netdef->address_options as only two of the addresses set options. And I might be missing something here, but I don't think we can know how many addresses set options in the middle of parsing. This seems to be an issue for the IPv4/IPv6 addresses as well, right? (i.e. we can't know how many of the keys will be IPv4 and how many will be IPv6 until we're done parsing all of them).

I've added a draft of how we could do this for address options in a separate commit, but it's not a very elegant solution imho (it also breaks check-coverage atm :). If we have no way of knowing that we've done a first pass already, I don't think we'll be able to implement something much different from that, unfortunately. What do you think? Any other ideas?

Copy link
Contributor

Choose a reason for hiding this comment

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

You are absolutely correct! Length comparison won't work out here, due to having MAPPING and SCALAR nodes handled differently.

You draft is looking pretty good already.
Well, I guess we could introduce a new global variable, which increments with each new parsing pass, to know number of the current pass. But this would be very abstract and might confuse people... I think I'd like to keep the multi-pass handling local to this function and array, so it can be easily understood.

Allocating a new cur_addr_option, processing all it's mappings, comparing each field inside via addr_options_equals and free'ing it up afterwards seems like lots of overhead, though. I think we should move the check prior to allocating new memory for cur_addr_option and just compare the opts->address VS scalar(key), which we already have at that point. This way we know if there are any options set for this address and can skip it's handling completely.

I prepared a test-case, adding a bridge to trigger multi-pass parsing. Also, I drafted my thoughts from above in this commit: slyon@1734de6

Feel free to cherry-pick and/or adopt.

Copy link

@vorlonofportland vorlonofportland left a comment

Choose a reason for hiding this comment

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

General +1 on the schema change. Some comments inline.

@hrasiq
Copy link
Contributor Author

hrasiq commented Jul 27, 2020

@slyon @vorlonofportland Thanks a lot for the reviews! I've pushed a new version, fixing most of your comments. I left minor comments on some of the previous reviews, but please let me know if I've missed anything!

@hrasiq hrasiq force-pushed the master branch 2 times, most recently from 4f7974c to bafa8ce Compare July 29, 2020 19:46
Copy link
Contributor

@slyon slyon left a comment

Choose a reason for hiding this comment

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

@hrasiq Thanks, this PR is shaping up nicely!

I've added two tiny inline formatting suggestions. And drafted an improvement for your multi-pass handling here:
slyon@1734de6

Let me know what you think!

if (!process_mapping(doc, value, address_option_handlers, error))
return FALSE;

g_array_append_val(cur_netdef->address_options, cur_addr_option);
Copy link
Contributor

Choose a reason for hiding this comment

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

You are absolutely correct! Length comparison won't work out here, due to having MAPPING and SCALAR nodes handled differently.

You draft is looking pretty good already.
Well, I guess we could introduce a new global variable, which increments with each new parsing pass, to know number of the current pass. But this would be very abstract and might confuse people... I think I'd like to keep the multi-pass handling local to this function and array, so it can be easily understood.

Allocating a new cur_addr_option, processing all it's mappings, comparing each field inside via addr_options_equals and free'ing it up afterwards seems like lots of overhead, though. I think we should move the check prior to allocating new memory for cur_addr_option and just compare the opts->address VS scalar(key), which we already have at that point. This way we know if there are any options set for this address and can skip it's handling completely.

I prepared a test-case, adding a bridge to trigger multi-pass parsing. Also, I drafted my thoughts from above in this commit: slyon@1734de6

Feel free to cherry-pick and/or adopt.

hrasiq and others added 2 commits July 31, 2020 12:44
This patch introduces a way to set options for the "[Address]" section
in the networkd backend. It changes the addresses YAML key to a sequence
of scalars and mappings, keeping backward compatibility with the
previous address notation.

Signed-off-by: Heitor Alves de Siqueira <[email protected]>
@hrasiq
Copy link
Contributor Author

hrasiq commented Jul 31, 2020

@slyon Thanks for the feedback! You're correct, doing it before allocating and
parsing is obviously preferable :)

I did a minor tweak to your draft I would like to get your opinion on: instead
of continuing to the next address, could we just return early if we detect
options for a specific address have been set already? We shouldn't need to try
to parse all of them again, as addresses without options should have been set as
well, right? What do you think?

Copy link
Contributor

@slyon slyon left a comment

Choose a reason for hiding this comment

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

Thank you @hrasiq !
I cannot think of any case where the IP addresses (or their options) would not be parsed in the same pass and all our test cases pass with your shortcut applied. So I think we should be fine to keep the optimization.

I do not have any other concerns regarding your PR.
LGTM!

I will check back with the team and will probably merge it soon. Thank you very much for your work, it is highly appreciated!

@slyon
Copy link
Contributor

slyon commented Aug 4, 2020

Passes all integration- and unit-tests.
Merged.

@slyon slyon merged commit 85134d1 into canonical:master Aug 4, 2020
@slyon slyon mentioned this pull request May 29, 2024
5 tasks
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.

6 participants