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

feat(net): provide network config to netplan.State for render #4981

Conversation

blackboxsw
Copy link
Collaborator

@blackboxsw blackboxsw commented Feb 29, 2024

Proposed Commit Message

feat(net): provide network config to netplan.State for render (#4981)

Rely on netplan API where present to write network config to
50-cloud-init.yaml. (SC-1402)

Allow cloud-init to be decoupled from security policies of netplan
libraries when rendering potentially senstive network configuratiion.

Newer netplan versions may filter sensitive network configuration parts
into separate files or subdirectories. Allow cloud-init to rely on
netplan's treatment of sensitive keys by using netplan.state.State to
write_yaml_file with cloud-init's requested base filename:
50-cloud-init.yaml.

If netplan security policy changes in the future, those changes will
be adopted under the hood of netplan.State.

Additional Context

Some concerns with this approach:

  1. Given that netplan API doesn't yet surface a public method, we are using _write_yaml_file under expectation that this interface does't change (needs netplan team sign-off for awareness) CC: @slyon
  2. The current State._write_yaml_file doesn't accept a header, strips any inline comments when processing network config yaml. This means that 50-cloud-init.yaml, as rendered by netplan's State._write_yaml will no long contain the helpful comment/header:
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}

If we want to take this approach to use netplan API to avoid policy changes in netplan, we probably should look to a feature request for netplan.State._write_yaml_file to allow a header or footer parameter to allow extending the config content that is written by netplan.

Test Steps

Checklist

Merge type

  • Squash merge using "Proposed Commit Message"
  • Rebase and merge unique commits. Requires commit messages per-commit each referencing the pull request number (#<PR_NUM>)

# determine default root-dir /etc/netplan and/or specialized
# filenames or read permissions based on whether this config
# contains secrets.
state_output_file._write_yaml_file(
Copy link
Collaborator Author

@blackboxsw blackboxsw Feb 29, 2024

Choose a reason for hiding this comment

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

@slyon points here for awareness:

  1. cloud-init is calling this private method from netplan in absence of a public netplan API intended to let netplan choose ultimately what file to write and in which directories. Can we consider this a safe method to call that won't change until cloud-init is able to call a public method in the future?
  2. If netplan grows the ability in the future, do you expect the functionality to separate sensitive config from world-readable config will live under this method call?

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Yes
  2. Yes

There is an idea of introducing a public .write_yaml() method. But we have to work out some kinks first. _write_yaml_file() is good for now, as its underlying CAPI is a public symbol netplan_state_write_yaml_file(). And it's also what is being used in the Netplan CLI.

Copy link
Collaborator Author

@blackboxsw blackboxsw Feb 29, 2024

Choose a reason for hiding this comment

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

@slyon excellent and thank you. I was wondering what you thought about a feature request for an optional header param to _write_yaml_file in the future that would allow cloud-init to inject custom comments into the file being written?

Copy link
Contributor

Choose a reason for hiding this comment

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

I was already pondering about ways to keep the comments, but that's not currently possible with the underlying libyaml. I like the idea of a header parameter.

Copy link
Member

Choose a reason for hiding this comment

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

Another thing that might be helpful is returning the path(s) of any files written so that at the very least we could add our own header after the fact.

@TheRealFalcon TheRealFalcon self-assigned this Feb 29, 2024
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.

lgtm

Comment on lines 263 to 256
cfg_dir = mkdtemp()
try:
with open(os.path.join(cfg_dir, "ci-net-cfg.yaml"), "wb+") as f:
f.write(util.encode_text(net_config_content))
Copy link
Contributor

Choose a reason for hiding this comment

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

note (non-blocking): You could consider doing this all in-memory, using io.StringIO and/or os.memfd_create

# determine default root-dir /etc/netplan and/or specialized
# filenames or read permissions based on whether this config
# contains secrets.
state_output_file._write_yaml_file(
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Yes
  2. Yes

There is an idea of introducing a public .write_yaml() method. But we have to work out some kinks first. _write_yaml_file() is good for now, as its underlying CAPI is a public symbol netplan_state_write_yaml_file(). And it's also what is being used in the Netplan CLI.

Copy link
Member

@TheRealFalcon TheRealFalcon left a comment

Choose a reason for hiding this comment

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

Generally LGTM. I do agree with slyon that we should use something like StringIO or perhaps a SpooledTemporaryFile rather than the current tmpdir approach.

Also, will netplan currently write a separate private config for the sensitive parts? If so, I think it's worth a separate integration test ensuring the pieces get written correctly.

return False
cfg_dir = mkdtemp()
try:
with open(os.path.join(cfg_dir, "ci-net-cfg.yaml"), "wb+") as f:
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if this will still be relevant if you switch to StringIO, but why are we opening binary if we're writing text? We can pass the encoding to open() (or just not specify an encoding, but that's considered bad practice) rather than using a utility function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is resolved/dropped as I'm now using SpooledTemporaryFile which defaults to "w+b" and ensuring we are encoding as bytes

# determine default root-dir /etc/netplan and/or specialized
# filenames or read permissions based on whether this config
# contains secrets.
state_output_file._write_yaml_file(
Copy link
Member

Choose a reason for hiding this comment

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

Another thing that might be helpful is returning the path(s) of any files written so that at the very least we could add our own header after the fact.

mode = current_mode
util.write_file(fpnplan, content, mode=mode)
if not netplan_api_write_yaml_file(content):
# No netplan api, fallback to write config directly.
Copy link
Member

Choose a reason for hiding this comment

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

Nit: As we're adding some extra complexity here, I think the code under this if block would be better as its own function. fallback_write_yaml_file or something like that.

@slyon
Copy link
Contributor

slyon commented Mar 12, 2024

Also, will netplan currently write a separate private config for the sensitive parts? If so, I think it's worth a separate integration test ensuring the pieces get written correctly.

Netplan does not currently do that. It's just a card in our backlog so far, see FR-3285 for more details.

@github-actions github-actions bot added the stale-pr Pull request is stale; will be auto-closed soon label Mar 27, 2024
@github-actions github-actions bot closed this Apr 3, 2024
@blackboxsw blackboxsw reopened this Apr 15, 2024
@blackboxsw blackboxsw force-pushed the SC-1402-use-netplan-api-to-write-50-cloud-init.yaml branch from 2756d2c to 0ad5268 Compare April 16, 2024 00:00
Rely on netplan API, where present, to write network config
to 50-cloud-init.yaml.

Allow cloud-init to be decoupled from security policies of netplan
libraries when rendering potentially senstive network configuratiion.

Newer netplan versions may filter sensitive network configuration
parts into separate files or directories. Allow cloud-init to
rely on netplan's rendering behavior by using netplan.state.State
to write_yaml_file with cloud-init's requested base filename:
50-cloud-init.yaml.

If netplan security policy changes in the future, those changes
will be honored under the hood of netplan.State.
@blackboxsw blackboxsw force-pushed the SC-1402-use-netplan-api-to-write-50-cloud-init.yaml branch from 0ad5268 to 416dd69 Compare April 16, 2024 00:03
@blackboxsw blackboxsw removed the stale-pr Pull request is stale; will be auto-closed soon label Apr 16, 2024
@canonical canonical deleted a comment from github-actions bot Apr 16, 2024
@TheRealFalcon
Copy link
Member

I think we can simplify a bit more, can't we?

diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
index fe97fdcc9..46cbe3a9d 100644
--- a/cloudinit/net/netplan.py
+++ b/cloudinit/net/netplan.py
@@ -250,11 +250,9 @@ def netplan_api_write_yaml_file(net_config_content: str) -> bool:
             CLOUDINIT_NETPLAN_FILE,
         )
         return False
-    cfg_dir = mkdtemp()
     try:
-        with SpooledTemporaryFile(dir=cfg_dir, mode="w+b") as f:
-            f.write(util.encode_text(net_config_content))
-            f.flush()
+        with SpooledTemporaryFile(mode="w") as f:
+            f.write(net_config_content)
             f.seek(0, io.SEEK_SET)
             parser = Parser()
             parser.load_yaml(f)
@@ -276,8 +274,6 @@ def netplan_api_write_yaml_file(net_config_content: str) -> bool:
             e,
         )
         return False
-    if os.path.exists(cfg_dir):
-        shutil.rmtree(cfg_dir)
     LOG.debug("Rendered netplan config using netplan python API")
     return True
 

@blackboxsw
Copy link
Collaborator Author

@TheRealFalcon correct, we can simplify this. I also adapted our netplan config delta check to allow it to be used for both netplan API and fallback method to ensure we don't run netplan generate unnecessarily when netplan config hasn't changed.
CLOUD_INIT_CLOUD_INIT_SOURCE=IN_PLACE CLOUD_INIT_OS_IMAGE=jammy .tox/integration-tests/bin/pytest -- tests/integration_tests/test_networking.py::TestNetplanGenerateBehaviorOnReboot::test_skip for testing.

Copy link
Member

@TheRealFalcon TheRealFalcon left a comment

Choose a reason for hiding this comment

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

+1 to the primary changes, but you have test failures now.

@blackboxsw blackboxsw force-pushed the SC-1402-use-netplan-api-to-write-50-cloud-init.yaml branch 2 times, most recently from 495d08c to e7386fa Compare April 19, 2024 04:34
Also check netplan config deltas for prior to using netplan API
to avoid netplan generate cmd when netplan conf hasn't changed.
@blackboxsw blackboxsw force-pushed the SC-1402-use-netplan-api-to-write-50-cloud-init.yaml branch from e7386fa to ddf676d Compare April 19, 2024 16:02
Copy link
Member

@TheRealFalcon TheRealFalcon left a comment

Choose a reason for hiding this comment

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

LGTM!

Test nit: We don't really have any unit test coverage of the new functionality. But also:

  1. We have integration test coverage.
  2. The whole thing is essentially a wrapper to an outside call, so there's not really much value to a unit test.
  3. It's all wrapped in exceptions to fallback on failure.

Given these 3 things, I'm ok with it.

@blackboxsw blackboxsw merged commit c465de8 into canonical:main Apr 20, 2024
28 checks passed
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