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

cli:set: update only specific origin-hint if given (LP: #1997467) #299

Merged
merged 5 commits into from
Jan 26, 2023

Conversation

slyon
Copy link
Collaborator

@slyon slyon commented Nov 24, 2022

Description

This PR solves multiple issues in netplan set. First, deletion of origin-hint files on update (i.e. if the netdef is already contained inside the origin-hint file). This happens when repeatedly calling netplan set on the same origin-hint and netdef, e.g.:

    Starting with an empty Netplan config in /etc/netplan:
    $ netplan set --origin-hint test "bridges.br54.dhcp4=false"
    => will create test.yaml, containing the br54 definition
    $ netplan set --origin-hint test "bridges.br54.dhcp4=true"
    => updating the same br54 netdef (re-using 'netplan set') will delete test.yaml

This is fixed by the first commit:

  • 8931bf3 generate:util: fix double-slash root filepath

Next, when parsing the full YAML hierarchy, stanza updates might be siphoned away, as the same interface might have been defined in another file (e.g. 0-snapd-defaults.yaml) and therefore any updates are redirected to that previous file, or ignored if an origin-hint is passed, which does not match the previous filename (e.g. origin-hint: 90-snapd-config.yaml). Still we always need to parse the full YAML hierarchy in order to validate our data. For example, somebody might call netplan set --origin-hint=test "ethernets.br54.interfaces=[eth0]", which will end up having the new br54 data in hint.yaml but still relies on a definition of eth0 in some other YAML file. If somebody calls netplan set --origin-hint=test "ethernets.eth0.dhcp4=true" OTOH, we want to ignore any previous definition of eth0 and write our update into hint.yaml (assuming it will be a higher order file, compared to the existing YAML hierarchy).
We implemented a load_nullable_overrides() function, to ignore global values or netdefs that are supposed to be written into an origin-hint file.

Last, we want global values (like "renderer") to be conserved per file, instead of written to any new file. When writing a new file (e.g. origin-hint.yaml) we want any renderer value inside the YAML hierarchy to stay unchanged. Also, we only want to add a renderer stanza to the new file if that was given via a "netplan set ..." YAML patch (or an already existing origin-hint file already contains this stanza). So we need to track the global values on a per YAML file base and confirm the filename/path before writing those.

Checklist

  • Runs make check successfully.
  • Retains 100% code coverage (make check-coverage).
  • New/changed keys in YAML format are documented.
  • (Optional) Adds example YAML for new feature.
  • (Optional) Closes an open bug in Launchpad. LP#1997467

Copy link
Contributor

@schopin-pro schopin-pro left a comment

Choose a reason for hiding this comment

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

See the only inline comment for details as to why this fix doesn't work :-(

Comment on lines 77 to 82
if self.origin_hint: # update only this specific file
path = os.path.join(self.root_dir, 'etc', 'netplan', filename)
if os.path.isfile(path):
parser.load_yaml(path)
else:
parser.load_yaml_hierarchy(self.root_dir)
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: this won't work in the general case

The only reason this works now is because you have standalone definitions. If your new definition refers to an interface defined in another file, netplan set will fail.

A quickfix would be to add a field to netdefs (in the private_data field) to mark them as "touched" when parsing a file without name (npp->current.file_path == NULL), and dump them out in write_yaml_file (or add another variant of this function with these semantics).

The problem with this quickfix is that you'll get the entire netdef dumped, not just the patched parts. A more involved fix would be to change the dirtiness tracker to store origin file rather than a simple boolean. This would allow for fine-grained control over which field we write out, but the implementation could get tricky (or very memory-hungry)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you very much for your review and hints. I need to allocate some extra time to dive deeper into this and fix it properly!

src/validation.c Outdated Show resolved Hide resolved
@slyon slyon force-pushed the slyon/set-regression-0105 branch 2 times, most recently from 81a8e72 to ecbc356 Compare December 12, 2022 16:18
@slyon slyon added the RFC Request for comment (don't merge yet) label Dec 12, 2022
@slyon
Copy link
Collaborator Author

slyon commented Dec 12, 2022

I think the problem described in LP#1997467 should be fixed now. In addition to fixing/improving the path handling (comparison/matching of "invalid" strings, starting with //) and the get/set handling of the global renderer field, depending on which file it was parsed from (this hadn't been tracked at all, up to now).

This PR still needs some refinement/cleanup/comments/proper commit messages and maybe handling of additional edge cases, like the global version or openvswitch fields. But maybe @schopin-pro and/or @daniloegea might already have a brief look at it, considering it a RFC.

@schopin-pro
Copy link
Contributor

I'm not sure I fully grasp the new approach, but I have a first question in mind:

What happens if I have this file:

# read-only file
network:
  ethernets:
    eth0:
      dhcp4: true

and I do netplan set --origin-hint=writable.yaml ethernets.eth0=null ?

@slyon slyon force-pushed the slyon/set-regression-0105 branch 2 times, most recently from 5ba1b28 to c4b317b Compare January 12, 2023 15:09
@slyon
Copy link
Collaborator Author

slyon commented Jan 12, 2023

I'm not sure I fully grasp the new approach, but I have a first question in mind:

What happens if I have this file:

# read-only file
network:
  ethernets:
    eth0:
      dhcp4: true

and I do netplan set --origin-hint=writable.yaml ethernets.eth0=null ?

I think this example has a flaw.. we're generally expecting read/write permissions on the config files (at least for the owner/root), as of course we cannot update or modify read-only files and would error out.

Assuming that the "read-only file" pasted above is not the same as writable.yaml, this wouldn't matter in this case, though. If the --origin-hint=writable.yaml parameter is given, it means that Netplan is supposed to only work/write on that specific origin-hint file, considering the rest of the YAML hierarchy as read-only, just for context/reference of dependent interface definitions.

Basically, this netplan set commands means: "Delete eth0 from writable.yaml".
If writable.yaml exists (and contains an "eth0" definition) this definition would be deleted.
If writable.yaml doesn't exist, it would not exist after executing the command.
The read-only_file.yaml would stay untouched at all times.

@slyon slyon force-pushed the slyon/set-regression-0105 branch 2 times, most recently from 4276f96 to d28b2ce Compare January 16, 2023 15:08
@slyon slyon removed the RFC Request for comment (don't merge yet) label Jan 16, 2023
Copy link
Contributor

@schopin-pro schopin-pro left a comment

Choose a reason for hiding this comment

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

You're almost there, but there are still holes in your solution.

There's a way to paper over said holes by parsing twice the data, once for validation and once for rendering, but that's only a short-term kludge. The underlying issue is the baked-in assumption that netdef is defined only in one place, whereas even individual pieces of data can have several different origins. Fixing the root cause would take much more efforts though, so let's fix the issue at hand first :)

netplan/cli/commands/set.py Outdated Show resolved Hide resolved
netplan/cli/commands/set.py Outdated Show resolved Hide resolved
netplan/cli/commands/set.py Outdated Show resolved Hide resolved
src/netplan.c Outdated Show resolved Hide resolved
@slyon slyon force-pushed the slyon/set-regression-0105 branch 2 times, most recently from cc2ec7c to f895608 Compare January 23, 2023 16:08
Copy link
Contributor

@schopin-pro schopin-pro left a comment

Choose a reason for hiding this comment

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

LGTM to me, I just have the usual couple of stray thoughts and remarks but nothing blocking, feel free to ignore them and merge :)

netplan/cli/commands/set.py Outdated Show resolved Hide resolved
netplan/cli/commands/set.py Outdated Show resolved Hide resolved
netplan/cli/commands/set.py Outdated Show resolved Hide resolved
Make sure we're using proper paths values (i.e. NOT "//etc/netplan/...")
as we're doing string comparisons on those values and the leading
double-slash might confuse those checks.

E.g. in netplan.c:netplan_state_write_yaml_file() we compare netdef->filepath
to the filepath of a new YAML file to be written. If a netdef has been parsed
but its filepath is using double slashes, it won't match the new file and the
new file will be deleted instead of updated.

Starting with an empty Netplan config in /etc/netplan:
$ netplan set --origin-hint test "bridges.br54.dhcp4=false"
=> will create test.yaml, containing the br54 definition
$ netplan set --origin-hint test "bridges.br54.dhcp4=true"
=> updating the same br54 netdef (re-using 'netplan set') will delete test.yaml
When parsing the full YAML hierarchy (from 'netplan set'), new settings might
not end up in the "to_write" GList (and therefore not in the new origin-hint
file), as the same interface might have been defined in another, previous file
(e.g. 0-snapd-defaults.yaml) and therefore any updates are redirected to that
previous file, or ignored if a origin-hint is passed, which does not match
the previous filename (e.g. origin-hint: 90-snapd-config.yaml).

Still we need to parse the full YAML hierarchy for validation reasons, as a new
origin-hint file might describe new definitions of an interface that contains
references to a netdef from another YAML file. E.g.:

$ cat 0-snapd-defaults.yaml
network:
  ethernets:
    eth0:
      dhcp4: true

$ netplan set --origin-hint=90-snapd-config "bridges.br54.interfaces=[eth0]"

=> This would fail if we wouldn't parse the full YAML hierarchy, before writing
the 90-snapd-config.yaml file, as it would not be aware of the "eth0" netdef.
…nored)

Add a new API function to allow loading nullable origin-hint overrides, i.e.
all global values (like "renderer", ...) or Netdef-IDs that are part of a given
YAML patch, and are supposed to be overridden inside the yaml hierarchy by the
resulting origin_hint/output file. Overrides (depending on YAML hierarchy) can
only happen on global values or on the individual netdef level.

The following example shows how this would be represented in the datastructure:
$ netplan set --origin-hint=hint "ethernets.eth0={dhcp4: false, dhcp6: NULL}"

Origin-hint filename: hint.yaml
YAML patch:
```
network:
  ethernets:
    eth0:
      dhcp4: false
      dchp6: NULL
```

The "null_fields" hashmap would contain one unconstrained entry for "eth0",
i.e. a delete operation:
network.ethernets.eth0.dhcp4 => NULL (via load_nullable_fields)

The "null_overrides" hashmap would contain one entry for the "eth0" Netdef,
contstrained by the origin-hint (i.e. to be ignored only if parsed from files
other than "hint.yaml"):
network.ethernets.eth0 => hint.yaml  (via load_nullable_overrides)
When using the new libnetplan YAML parser, we need to ignore any global values
(like "renderer") or netdefs (that are part of a given YAML patch) when parsing
the existing YAML hierarchy, unless they're about to be parsed from an existing
origin-hint file of the same name.

That is to avoid assigning updates to those stanzas/netdefs to existing network
definitions inside other files (not the origin-hint), as this would lead to
those updates being ignored when we're writing out just the single origin-hint
file (using state.write_yaml_file()). We still need to parse and validate the
full context of other netdefs (not part of a given YAML patch), as the patch
might reference existing netdefs (e.g. as bridge interfaces), or change current
configuration in a way that makes it invalid (e.g. changing vrf tables),
therefore they need to exist in the (validation-)parser's context. Also we
need to parse such netdefs from an origin-hint file of same name, as we'd want
update the definition in that file (instead of overriding it).

We do this by adding relevant global values and netdefs, which are part of the
YAML patch, to the "null_overrides" data, before parsing the full YAML
hierarchy. Adding the filename of the origin-hint as a constraint, which can be
checked when parsing to not ignore relevant netdefs from an exsisting
origin-hint file.
When parsing the global renderer, we also want to store the path+filename of
the parsed value in a new ->global_renderer datastructure (in parser & state).
This way, when generating YAML output, we can put a different/new renderer
stanza (e.g. from "netplan set network.renderer=...") into a higher order YAML
file while conserving the renderer data in the original YAML files.

The renderer can be parsed from an existing file or a new, unnamed YAML patch.
If the path is known, we want to write back the data to that file only. If the
path is unknown/anonymous (i.e. emtpy string) we want to write that data to the
fallback file (e.g. 70-netplan-set.yaml or <origin-hint>.yaml).
@slyon
Copy link
Collaborator Author

slyon commented Jan 26, 2023

Thank you for all the input. CI is passing and I verified the reproducers from LP#1997467 are passing (using PPA build: https://launchpad.net/~slyon/+archive/ubuntu/netplan-set-lp1997467)

Merging.

@slyon slyon merged commit d8659bb into main Jan 26, 2023
@slyon slyon deleted the slyon/set-regression-0105 branch January 26, 2023 13:12
balabit-sync pushed a commit to balabit-deps/balabit-os-9-netplan.io that referenced this pull request Mar 21, 2023
netplan.io (0.105-0ubuntu2~22.04.3) jammy; urgency=medium

  * Fix and improvements for the DBus integration (LP: #1997467)
    Cherry-picked from upstream: canonical/netplan#331
    - d/p/lp1997467/0009-dbus-Build-the-copy-path-correctly.patch
      Properly build the destination path before copying files in the dbus
      integration and improve error handling
    - d/p/lp1997467/0010-tests-Add-an-integration-test-for-netplan-dbus.patch
      Add an integration test to exercise the code path where the issue was
      addressed.

netplan.io (0.105-0ubuntu2~22.04.2) jammy; urgency=medium

  * d/p/lp1997467: set only specific origin-hint if given (LP: #1997467)
    Cherry-picked from upstream: canonical/netplan#299
    - d/libnetplan0.symbols: Add netplan_parser_load_nullable_overrides() API
    - d/p/0008-src-parse-plug-memory-leaks-in-nullable-handling.patch backport
      upstream commit 40c53bb (memory leak fixup of PR#299)
slyon added a commit to slyon/netplan that referenced this pull request Jul 19, 2023
…27584)

The "unlink" part at the bottom of netplan.c:netplan_state_update_yaml_hierarchy
seems to ignore any global values (such as "renderer") and operates on netdefs only.

The issue is happens due to a combination of the following PRs:
canonical#254
canonical#299

Which got SRUed into Jammy via 0.105-0ubuntu2~22.04.2

Here's a minimal reproducer:
```
netplan set network.renderer=NetworkManager --origin-hint=00-no-netdefs-just-globals
netplan set network.ethernets.eth99.dhcp4=true --origin-hint=90-some-netdefs
ls -l /etc/netplan/
netplan set network.ethernets.eth99=NULL
cat /etc/netplan/00-no-netdefs-just-globals.yaml
```

FR-4793
slyon added a commit to slyon/netplan that referenced this pull request Jul 20, 2023
…27584)

The "unlink" part at the bottom of netplan.c:netplan_state_update_yaml_hierarchy
seems to ignore any global values (such as "renderer") and operates on files
containing netdefs only.

The issue is happens due to a combination of the following PRs:
canonical#254
canonical#299

Which got SRUed into Jammy via 0.105-0ubuntu2~22.04.2

Here's a minimal reproducer:
```
netplan set network.renderer=NetworkManager --origin-hint=00-no-netdefs-just-globals
netplan set network.ethernets.eth99.dhcp4=true --origin-hint=90-some-netdefs
ls -l /etc/netplan/
netplan set network.ethernets.eth99=NULL
cat /etc/netplan/00-no-netdefs-just-globals.yaml
```

FR-4793
slyon added a commit to slyon/netplan that referenced this pull request Jul 20, 2023
…27584)

The "unlink" part at the bottom of netplan.c:netplan_state_update_yaml_hierarchy
seems to ignore any global values (such as "renderer") and operates on files
containing netdefs only.

The issue is happens due to a combination of the following PRs:
canonical#254
canonical#299

Which got SRUed into Jammy via 0.105-0ubuntu2~22.04.2

Here's a minimal reproducer:
```
netplan set network.renderer=NetworkManager --origin-hint=00-no-netdefs-just-globals
netplan set network.ethernets.eth99.dhcp4=true --origin-hint=90-some-netdefs
ls -l /etc/netplan/
netplan set network.ethernets.eth99=NULL
cat /etc/netplan/00-no-netdefs-just-globals.yaml
```

FR-4793
slyon added a commit to slyon/netplan that referenced this pull request Jul 24, 2023
…27584)

The "unlink" part at the bottom of netplan.c:netplan_state_update_yaml_hierarchy
seems to ignore any global values (such as "renderer") and operates on files
containing netdefs only.

The issue is happens due to a combination of the following PRs:
canonical#254
canonical#299

Which got SRUed into Jammy via 0.105-0ubuntu2~22.04.2

Here's a minimal reproducer:
```
netplan set network.renderer=NetworkManager --origin-hint=00-no-netdefs-just-globals
netplan set network.ethernets.eth99.dhcp4=true --origin-hint=90-some-netdefs
ls -l /etc/netplan/
netplan set network.ethernets.eth99=NULL
cat /etc/netplan/00-no-netdefs-just-globals.yaml
```

FR-4793
slyon added a commit that referenced this pull request Jul 24, 2023
…27584)

The "unlink" part at the bottom of netplan.c:netplan_state_update_yaml_hierarchy
seems to ignore any global values (such as "renderer") and operates on files
containing netdefs only.

The issue is happens due to a combination of the following PRs:
#254
#299

Which got SRUed into Jammy via 0.105-0ubuntu2~22.04.2

Here's a minimal reproducer:
```
netplan set network.renderer=NetworkManager --origin-hint=00-no-netdefs-just-globals
netplan set network.ethernets.eth99.dhcp4=true --origin-hint=90-some-netdefs
ls -l /etc/netplan/
netplan set network.ethernets.eth99=NULL
cat /etc/netplan/00-no-netdefs-just-globals.yaml
```

FR-4793
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.

3 participants