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 connectivity_url to Oracle's EphemeralDHCPv4 (SC-395) #988

Merged
merged 5 commits into from Sep 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 27 additions & 10 deletions cloudinit/net/__init__.py
Expand Up @@ -11,6 +11,7 @@
import logging
import os
import re
from typing import Any, Dict

from cloudinit import subp
from cloudinit import util
Expand Down Expand Up @@ -971,18 +972,33 @@ def get_ib_hwaddrs_by_interface():
return ret


def has_url_connectivity(url):
"""Return true when the instance has access to the provided URL
def has_url_connectivity(url_data: Dict[str, Any]) -> bool:
"""Return true when the instance has access to the provided URL.

Logs a warning if url is not the expected format.

url_data is a dictionary of kwargs to send to readurl. E.g.:

has_url_connectivity({
"url": "http://example.invalid",
"headers": {"some": "header"},
"timeout": 10
})
"""
if 'url' not in url_data:
LOG.warning(
"Ignoring connectivity check. No 'url' to check in %s", url_data)
TheRealFalcon marked this conversation as resolved.
Show resolved Hide resolved
return False
url = url_data['url']
if not any([url.startswith('http://'), url.startswith('https://')]):
LOG.warning(
"Ignoring connectivity check. Expected URL beginning with http*://"
" received '%s'", url)
return False
if 'timeout' not in url_data:
url_data['timeout'] = 5
try:
readurl(url, timeout=5)
readurl(**url_data)
except UrlError:
return False
return True
Expand Down Expand Up @@ -1025,14 +1041,15 @@ class EphemeralIPv4Network(object):

No operations are performed if the provided interface already has the
specified configuration.
This can be verified with the connectivity_url.
This can be verified with the connectivity_url_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.
"""

def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,
connectivity_url=None, static_routes=None):
connectivity_url_data: Dict[str, Any] = None,
static_routes=None):
"""Setup context manager and validate call signature.

@param interface: Name of the network interface to bring up.
Expand All @@ -1041,7 +1058,7 @@ def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,
prefix.
@param broadcast: Broadcast address for the IPv4 network.
@param router: Optionally the default gateway IP.
@param connectivity_url: Optionally, a URL to verify if a usable
@param connectivity_url_data: Optionally, a URL to verify if a usable
connection already exists.
@param static_routes: Optionally a list of static routes from DHCP
"""
Expand All @@ -1056,7 +1073,7 @@ def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,
'Cannot setup network: {0}'.format(e)
) from e

self.connectivity_url = connectivity_url
self.connectivity_url_data = connectivity_url_data
self.interface = interface
self.ip = ip
self.broadcast = broadcast
Expand All @@ -1066,11 +1083,11 @@ def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,

def __enter__(self):
"""Perform ephemeral network setup if interface is not connected."""
if self.connectivity_url:
if has_url_connectivity(self.connectivity_url):
if self.connectivity_url_data:
if has_url_connectivity(self.connectivity_url_data):
LOG.debug(
'Skip ephemeral network setup, instance has connectivity'
' to %s', self.connectivity_url)
' to %s', self.connectivity_url_data['url'])
return

self._bringup_device()
Expand Down
20 changes: 13 additions & 7 deletions cloudinit/net/dhcp.py
Expand Up @@ -4,6 +4,7 @@
#
# This file is part of cloud-init. See LICENSE file for license information.

from typing import Dict, Any
import configobj
import logging
import os
Expand Down Expand Up @@ -38,21 +39,26 @@ class NoDHCPLeaseError(Exception):


class EphemeralDHCPv4(object):
def __init__(self, iface=None, connectivity_url=None, dhcp_log_func=None):
def __init__(
self,
iface=None,
connectivity_url_data: Dict[str, Any] = None,
dhcp_log_func=None
):
self.iface = iface
self._ephipv4 = None
self.lease = None
self.dhcp_log_func = dhcp_log_func
self.connectivity_url = connectivity_url
self.connectivity_url_data = connectivity_url_data

def __enter__(self):
"""Setup sandboxed dhcp context, unless connectivity_url can already be
reached."""
if self.connectivity_url:
if has_url_connectivity(self.connectivity_url):
if self.connectivity_url_data:
if has_url_connectivity(self.connectivity_url_data):
LOG.debug(
'Skip ephemeral DHCP setup, instance has connectivity'
' to %s', self.connectivity_url)
' to %s', self.connectivity_url_data)
return
return self.obtain_lease()

Expand Down Expand Up @@ -104,8 +110,8 @@ def obtain_lease(self):
if kwargs['static_routes']:
kwargs['static_routes'] = (
parse_static_routes(kwargs['static_routes']))
if self.connectivity_url:
kwargs['connectivity_url'] = self.connectivity_url
if self.connectivity_url_data:
kwargs['connectivity_url_data'] = self.connectivity_url_data
ephipv4 = EphemeralIPv4Network(**kwargs)
ephipv4.__enter__()
self._ephipv4 = ephipv4
Expand Down
8 changes: 6 additions & 2 deletions cloudinit/net/tests/test_dhcp.py
Expand Up @@ -617,7 +617,9 @@ def test_ephemeral_dhcp_no_network_if_url_connectivity(self, m_dhcp):
url = 'http://example.org/index.html'

httpretty.register_uri(httpretty.GET, url)
with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease:
with net.dhcp.EphemeralDHCPv4(
connectivity_url_data={'url': url},
) as lease:
self.assertIsNone(lease)
# Ensure that no teardown happens:
m_dhcp.assert_not_called()
Expand All @@ -635,7 +637,9 @@ def test_ephemeral_dhcp_setup_network_if_url_connectivity(
m_subp.return_value = ('', '')

httpretty.register_uri(httpretty.GET, url, body={}, status=404)
with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease:
with net.dhcp.EphemeralDHCPv4(
connectivity_url_data={'url': url},
) as lease:
self.assertEqual(fake_lease, lease)
# Ensure that dhcp discovery occurs
m_dhcp.called_once_with()
Expand Down
20 changes: 13 additions & 7 deletions cloudinit/net/tests/test_init.py
Expand Up @@ -622,11 +622,14 @@ def test_ephemeral_ipv4_no_network_if_url_connectivity(
params = {
'interface': 'eth0', 'ip': '192.168.2.2',
'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
'connectivity_url': 'http://example.org/index.html'}
'connectivity_url_data': {'url': 'http://example.org/index.html'}
}

with net.EphemeralIPv4Network(**params):
self.assertEqual([mock.call('http://example.org/index.html',
timeout=5)], m_readurl.call_args_list)
self.assertEqual(
[mock.call(url='http://example.org/index.html', timeout=5)],
m_readurl.call_args_list
)
# Ensure that no teardown happens:
m_subp.assert_has_calls([])

Expand Down Expand Up @@ -850,25 +853,28 @@ def setUp(self):
def test_url_timeout_on_connectivity_check(self, m_readurl):
"""A timeout of 5 seconds is provided when reading a url."""
self.assertTrue(
net.has_url_connectivity(self.url), 'Expected True on url connect')
net.has_url_connectivity({'url': self.url}),
'Expected True on url connect')

def test_true_on_url_connectivity_success(self):
httpretty.register_uri(httpretty.GET, self.url)
self.assertTrue(
net.has_url_connectivity(self.url), 'Expected True on url connect')
net.has_url_connectivity({'url': self.url}),
'Expected True on url connect')

@mock.patch('requests.Session.request')
def test_true_on_url_connectivity_timeout(self, m_request):
"""A timeout raised accessing the url will return False."""
m_request.side_effect = requests.Timeout('Fake Connection Timeout')
self.assertFalse(
net.has_url_connectivity(self.url),
net.has_url_connectivity({'url': self.url}),
'Expected False on url timeout')

def test_true_on_url_connectivity_failure(self):
httpretty.register_uri(httpretty.GET, self.url, body={}, status=404)
self.assertFalse(
net.has_url_connectivity(self.url), 'Expected False on url fail')
net.has_url_connectivity({'url': self.url}),
'Expected False on url fail')


def _mk_v1_phys(mac, name, driver, device_id):
Expand Down
13 changes: 9 additions & 4 deletions cloudinit/sources/DataSourceOracle.py
Expand Up @@ -40,6 +40,7 @@
# https://docs.cloud.oracle.com/iaas/Content/Network/Troubleshoot/connectionhang.htm#Overview,
# indicates that an MTU of 9000 is used within OCI
MTU = 9000
V2_HEADERS = {"Authorization": "Bearer Oracle"}

OpcMetadata = namedtuple("OpcMetadata", "version instance_data vnics_data")

Expand Down Expand Up @@ -134,7 +135,13 @@ def _get_data(self):
)
network_context = noop()
if not _is_iscsi_root():
network_context = dhcp.EphemeralDHCPv4(net.find_fallback_nic())
network_context = dhcp.EphemeralDHCPv4(
iface=net.find_fallback_nic(),
connectivity_url_data={
"url": METADATA_PATTERN.format(version=2, path="instance"),
"headers": V2_HEADERS,
}
)
with network_context:
fetched_metadata = read_opc_metadata(
fetch_vnics_data=fetch_vnics_data
Expand Down Expand Up @@ -304,11 +311,9 @@ def read_opc_metadata(*, fetch_vnics_data: bool = False):
retries = 2

def _fetch(metadata_version: int, path: str) -> dict:
headers = {
"Authorization": "Bearer Oracle"} if metadata_version > 1 else None
return readurl(
url=METADATA_PATTERN.format(version=metadata_version, path=path),
headers=headers,
headers=V2_HEADERS if metadata_version > 1 else None,
retries=retries,
)._response.json()

Expand Down
2 changes: 1 addition & 1 deletion cloudinit/sources/helpers/vultr.py
Expand Up @@ -20,7 +20,7 @@
def get_metadata(url, timeout, retries, sec_between):
# Bring up interface
try:
with EphemeralDHCPv4(connectivity_url=url):
with EphemeralDHCPv4(connectivity_url_data={"url": url}):
# Fetch the metadata
v1 = read_metadata(url, timeout, retries, sec_between)
except (NoDHCPLeaseError) as exc:
Expand Down
10 changes: 9 additions & 1 deletion cloudinit/sources/tests/test_oracle.py
Expand Up @@ -694,7 +694,15 @@ def assert_in_context_manager(**kwargs):
assert oracle_ds._get_data()

assert [
mock.call(m_find_fallback_nic.return_value)
mock.call(
iface=m_find_fallback_nic.return_value,
connectivity_url_data={
'headers': {
'Authorization': 'Bearer Oracle'
},
'url': 'http://169.254.169.254/opc/v2/instance/'
}
)
] == m_EphemeralDHCPv4.call_args_list


Expand Down