fix (EC2/GCE): wait for NIC on allowlisted platforms - #7065
Conversation
blackboxsw
left a comment
There was a problem hiding this comment.
Thank you for all this work @goldberl! I think this looks like a contained approach which limits exposure to boot-time costs to known instance types.
I have a number of requests and discussion points inline. Please do feel free to push back on things that you think are unreasonable.
Can you also please add a comment with the related log entries emitted by this PR on a working instance?
9ae9014 to
8517063
Compare
|
Hi @blackboxsw, thank you for the detailed review. I've made the following changes to the PR:
I'll run tests on AWS and GCP instances to capture updated logs and update the PR description with those |
8517063 to
29aa35b
Compare
…latforms On some GCE and AWS EC2 instances, cloud-init-local runs before network interfaces are fully initialized by the kernel. This causes early datasource detection to fail, preventing metadata fetching and SSH access. To avoid introducing boot delays across all platforms, implement allowlist gated NIC polling and centralize the retry logic in a shared network helper: * add wait_for_candidate_nics() in cloudinit/net/__init__.py * use helper from DataSourceEc2 and DataSourceGCE only when allowlisted * EC2 gate: DMI system-product-name (e.g. hpc7a.96xlarge) * GCE gate: DMI baseboard-product-name (e.g. izumi) Fixes first-boot race conditions on affected AWS and GCP instances without impacting unaffected instance types or breaking unit tests. Add unit coverage for: * helper retry/timeout behavior * EC2 allowlisted vs non-allowlisted polling paths * GCE allowlisted vs non-allowlisted polling paths Fixes: canonicalGH-6697, canonicalGH-6737, LP-2144694 Signed-off-by: Leah Goldberg <leah.goldberg@canonical.com>
29aa35b to
f9d5234
Compare
blackboxsw
left a comment
There was a problem hiding this comment.
@goldberl thank you for this iteration.
While looking at the end product here, the use of timeout as dual purpose where 0 means just run once and > 0 means max_wait it makes it a bit harder to discern the intent of the timeout param.
I think we may need to go with the alternative solution to pre-flight check:
if not wait_on_nics:
find_candidate_nics()
else:
wait_on_candidate_nics()
That should then ensure a clear path between conditions which require calling wait_for_candidate_nics versus those we expect to succeed on a single call.
| break | ||
|
|
||
| LOG.debug( | ||
| "No primary NICs found with access to IMDS, waiting %s " |
There was a problem hiding this comment.
We don't truly know yet if IMDS is accessible, just that the NIC has carrier. So let's be more 'correct' about this in the logged line.
| "No primary NICs found with access to IMDS, waiting %s " | |
| "No primary NICs found with carrier, waiting %s " |
| "Timed out after %s seconds waiting for primary NICs with access " | ||
| "to IMDS", |
There was a problem hiding this comment.
Same with this message, IMDS may or may not be present on certain platforms. But, waiting on carrier is specifically what we are doing.
| LOG.debug("FreeBSD doesn't support running dhclient with -sf") | ||
| return False | ||
| candidate_nics = net.find_candidate_nics() | ||
| product_name = dmi.read_dmi_data("system-product-name") |
There was a problem hiding this comment.
We may want to .lower() to ensure we don't get hit by case-sensitivity.
|
|
||
| def test_timeout_logs_when_timeout_is_exceeded(self, mocker, caplog): | ||
| mocker.patch("cloudinit.net.find_candidate_nics", return_value=[]) | ||
| mocker.patch("cloudinit.net.time.sleep") |
There was a problem hiding this comment.
We are mocking time.sleep in these unittests but not time.monotonic which means we leak calls to the underlying host during these test runs which adds runtime to our unittests. Please mock time.monotonic where appropriate and validate that we don't block for 1 second while running tox -e py3 -- tests/unittests/net/test_init.py
| wait_for_nics = board_name in NETWORK_POLLING_BOARD_ALLOW_LIST | ||
| if wait_for_nics: | ||
| LOG.debug( | ||
| "Board %s is in NIC polling allowlist, waiting for NICs", |
There was a problem hiding this comment.
Let's generalize this log Waiting for NICs with carrier flag. and pull it into wait_for_candidate_nics. No need to report our instance type/allowlist/etc. Let's avoid duplicating in GCE and Ec2 where possible.
| ) | ||
| candidate_nics = net.wait_for_candidate_nics( | ||
| timeout=60 if wait_for_nics else 0, sleep_interval=1 | ||
| ) |
There was a problem hiding this comment.
Thank you for this iteration @goldberl.
There are a couple of things that make we want to avoid this choice now:
- the duplication of this logging in both GCE and EC2 which really should be inside
wait_for_candidate_nics - the unclear dual-purpose of the
timeoutparam insidewait_for_candidate_nicswhere a 0 timeout is special and means don't retry, whereas the timeout > 0 meansmax_waittime oncefind_candidate_nicsor retries complete. - We are already defining a
wait_for_nicslocal anyway which allows GCE or Ec2 to avoid callingwait_for_candidate_nicsin conditions where is isn't planning on waiting. This avoids a perception in the future that Ec2 or GCE will be waiting in all cases, and then we need to parse the slightly obscuretimeout = 60 if wait_4_nix or 0and then interpret what that means by re-reading thewait_for_candidate_nicsimplementation.
Instead of this, I think we probably should just go with the prior alternative proposed:
set a local variable find_candidate_nics which will be either net.find_candidate_nics or net.wait_on_candidate_nics based on whether the platform is in ALLOW_LIST.
No need to addtiionally log "Instance Id in ALLOW_LIST" we can discern that based on a leading log "Waiting on candidate NICs with carrier" or something like that when wait_for_candidate_nics is called.
|
|
||
| def test_timeout_logs_when_timeout_is_exceeded(self, mocker, caplog): | ||
| mocker.patch("cloudinit.net.find_candidate_nics", return_value=[]) | ||
| mocker.patch("cloudinit.net.time.sleep") |
There was a problem hiding this comment.
We are mocking time.sleep in these unittests but not time.monotonic which means we leak calls to the underlying host during these test runs which adds runtime to our unittests. Please mock time.monotonic where appropriate and validate that we don't block for 1 second while running tox -e py3 -- tests/unittests/net/test_init.py
Proposed Commit Message
Additional info
This fix will emit logs on affected AWS and GCP instances when NIC polling is enabled, such as:
Merge type