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 oracle ipv6 single stack imds functionality #5785

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 164 additions & 33 deletions cloudinit/net/ephemeral.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
"""Module for ephemeral network context managers
"""
import contextlib
import json
import logging
from functools import partial
from typing import Any, Callable, Dict, List, Optional
from typing import Any, Callable, Dict, List, Optional, Tuple

import cloudinit.net as net
import cloudinit.netinfo as netinfo
from cloudinit.net.dhcp import NoDHCPLeaseError, maybe_perform_dhcp_discovery
from cloudinit.subp import ProcessExecutionError
from cloudinit.url_helper import UrlError, wait_for_url

LOG = logging.getLogger(__name__)

Expand All @@ -20,7 +22,7 @@ class EphemeralIPv4Network:

No operations are performed if the provided interface already has the
specified configuration.
This can be verified with the connectivity_url_data.
This can be verified with the connectivity_urls_data.
If unconnected, bring up the interface with valid ip, prefix and broadcast.
If router is provided setup a default route for that interface. Upon
context exit, clean up the interface leaving no configuration behind.
Expand Down Expand Up @@ -281,26 +283,26 @@ def __init__(
self,
distro,
iface=None,
connectivity_url_data: Optional[Dict[str, Any]] = None,
connectivity_urls_data: Optional[List[Dict[str, Any]]] = None,
dhcp_log_func=None,
):
self.iface = iface
self._ephipv4: Optional[EphemeralIPv4Network] = None
self.lease: Optional[Dict[str, Any]] = None
self.dhcp_log_func = dhcp_log_func
self.connectivity_url_data = connectivity_url_data
self.connectivity_urls_data = connectivity_urls_data or []
self.distro = distro
self.interface_addrs_before_dhcp = netinfo.netdev_info()

def __enter__(self):
"""Setup sandboxed dhcp context, unless connectivity_url can already be
reached."""
if self.connectivity_url_data:
if net.has_url_connectivity(self.connectivity_url_data):
for url_data in self.connectivity_urls_data:
if net.has_url_connectivity(url_data):
LOG.debug(
"Skip ephemeral DHCP setup, instance has connectivity"
" to %s",
self.connectivity_url_data,
url_data,
)
return
return self.obtain_lease()
Expand Down Expand Up @@ -404,13 +406,33 @@ def __init__(
interface,
ipv6: bool = False,
ipv4: bool = True,
connectivity_urls_data: Optional[List[Dict[str, Any]]] = None,
):
"""
Args:
distro: The distro object
interface: The interface to bring up
ipv6: Whether to bring up an ipv6 network
ipv4: Whether to bring up an ipv4 network
connectivity_urls_data: List of url data to use for connectivity
check before bringing up DHCPv4 ephemeral network.
ipv6_connectivity_check_callback: A callback to check for ipv6
connectivity. If provided, it is assumed that ipv6 networking
is preferred and ipv4 networking will be skipped if ipv6
connectivity to IMDS is successful. This function should return
the url that was reached to verify ipv6 connectivity (if
successful - otherwise it should return None).
"""
self.interface = interface
self.ipv4 = ipv4
self.ipv6 = ipv6
self.stack = contextlib.ExitStack()
self.state_msg: str = ""
self.distro = distro
self.connectivity_urls_data = connectivity_urls_data

# will be updated by the context manager
self.imds_reached_at_url: Optional[str] = None

def __enter__(self):
if not (self.ipv4 or self.ipv6):
Expand All @@ -419,34 +441,25 @@ def __enter__(self):
return self
exceptions = []
ephemeral_obtained = False
if self.ipv4:
try:
self.stack.enter_context(
EphemeralDHCPv4(
self.distro,
self.interface,
)
)
ephemeral_obtained = True
except (ProcessExecutionError, NoDHCPLeaseError) as e:
LOG.info("Failed to bring up %s for ipv4.", self)
exceptions.append(e)

a-dubs marked this conversation as resolved.
Show resolved Hide resolved
if self.ipv6:
try:
self.stack.enter_context(
EphemeralIPv6Network(
self.distro,
self.interface,
)
self.imds_reached_at_url = self._perform_connectivity_check()

if self.imds_reached_at_url:
LOG.debug("We already have connectivity to IMDS, skipping DHCP.")
else:
LOG.debug("No connectivity to IMDS, attempting DHCP setup.")
# first try to bring up ephemeral network for ipv4 (if enabled)
# then try to bring up ephemeral network for ipv6 (if enabled)
if self.ipv4:
ephemeral_obtained, exceptions = self._do_ipv4(
ephemeral_obtained, exceptions
)
ephemeral_obtained = True
if exceptions or not self.ipv4:
self.state_msg = "using link-local ipv6"
except ProcessExecutionError as e:
LOG.info("Failed to bring up %s for ipv6.", self)
exceptions.append(e)
if not ephemeral_obtained:
if self.ipv6:
ephemeral_obtained, exceptions = self._do_ipv6(
ephemeral_obtained, exceptions
)

if not self.imds_reached_at_url and not ephemeral_obtained:
# Ephemeral network setup failed in linkup for both ipv4 and
# ipv6. Raise only the first exception found.
LOG.error(
Expand All @@ -456,5 +469,123 @@ def __enter__(self):
raise exceptions[0]
return self

def _do_ipv4(
self, ephemeral_obtained, exceptions
) -> Tuple[str, List[Exception]]:
"""
Attempt to bring up an ephemeral network for ipv4 on the interface.

Args:
ephemeral_obtained: Whether an ephemeral network has already been
obtained
exceptions: List of exceptions encountered so far

Returns:
A tuple containing the updated ephemeral_obtained and
exceptions values
"""
try:
self.stack.enter_context(
EphemeralDHCPv4(
distro=self.distro,
iface=self.interface,
)
)
ephemeral_obtained = True
LOG.debug(
"Successfully brought up %s for ephemeral ipv4 networking.",
self.interface,
)
except (ProcessExecutionError, NoDHCPLeaseError) as e:
LOG.debug(
"Failed to bring up %s for ephemeral ipv4 networking.",
self.interface,
)
# we don't set ephemeral_obtained to False here because we want to
# retain a potential true value from any previous successful
# ephemeral network setup
exceptions.append(e)
return ephemeral_obtained, exceptions

def _do_ipv6(
self, ephemeral_obtained, exceptions
) -> Tuple[str, List[Exception]]:
"""
Attempt to bring up an ephemeral network for ipv6 on the interface.

Args:
ephemeral_obtained: Whether an ephemeral network has already been
obtained
exceptions: List of exceptions encountered so far

Returns:
tupleA tuple containing the updated ephemeral_obtained and
exceptions values
"""
try:
self.stack.enter_context(
EphemeralIPv6Network(
self.distro,
self.interface,
)
)
ephemeral_obtained = True
if exceptions or not self.ipv4:
self.state_msg = "using link-local ipv6"
LOG.debug(
"Successfully brought up %s for ephemeral ipv6 networking.",
self.interface,
)
except ProcessExecutionError as e:
LOG.debug(
"Failed to bring up %s for ephemeral ipv6 networking.",
self.interface,
)
# we don't set ephemeral_obtained to False here because we want to
# retain a potential true value from any previous successful
# ephemeral network setup
exceptions.append(e)
return ephemeral_obtained, exceptions

def _perform_connectivity_check(
self,
) -> Optional[str]:

def headers_cb(url):
headers = [
url_data.get("headers") for url_data in self.connectivity_urls_data
if url_data["url"] == url
][0]
return headers

try:
url_that_worked, url_response = wait_for_url(
urls=[url_data["url"] for url_data in self.connectivity_urls_data],
headers_cb=headers_cb,
timeout=0.5, # keep really short for quick failure path
connect_synchronously=False,
)
imds_data = json.loads(url_response.decode("utf-8"))
except UrlError as e:
LOG.debug(
"Failed to reach IMDS with error: %s",
e,
)
except Exception as e: # pylint: disable=broad-except
LOG.debug(
"Unexpected error occurred. Failed to reach IMDS: %s",
e,
)
else:
if imds_data:
LOG.debug(
"IMDS was successfully reached at %s without ephemeral "
"network setup.",
url_that_worked,
)
return url_that_worked
LOG.debug("Failed to reach IMDS without ephemeral network setup.")
return None

def __exit__(self, *_args):
self.stack.close()
8 changes: 5 additions & 3 deletions cloudinit/sources/DataSourceHetzner.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ def _get_data(self):
with EphemeralDHCPv4(
self.distro,
iface=net.find_fallback_nic(),
connectivity_url_data={
"url": BASE_URL_V1 + "/metadata/instance-id",
},
connectivity_urls_data=[
{
"url": BASE_URL_V1 + "/metadata/instance-id",
}
],
):
md = hc_helper.read_metadata(
self.metadata_address,
Expand Down
8 changes: 5 additions & 3 deletions cloudinit/sources/DataSourceNWCS.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ def get_metadata(self):
with EphemeralDHCPv4(
self.distro,
iface=net.find_fallback_nic(),
connectivity_url_data={
"url": BASE_URL_V1 + "/metadata/instance-id",
},
connectivity_urls_data=[
{
"url": BASE_URL_V1 + "/metadata/instance-id",
}
],
):
return read_metadata(
self.metadata_address,
Expand Down
Loading
Loading