Support datasource specific instance metadata
This change aligns the instance metadata with cloudinit to add the support for datasource specific instance metadata. The datasource specific instance metadata allows more information to be exposed to the Jinja template for userdata and script. Note: The structure is not standarized but you may refer to https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html to the cloudinit format. Change-Id: I1ec7e5bdf063709c513b52a02c9251752aafe157
This commit is contained in:
parent
c10ff6896e
commit
dc0367425a
@ -28,6 +28,9 @@ from cloudbaseinit.utils import encoding
|
|||||||
|
|
||||||
CONF = cloudbaseinit_conf.CONF
|
CONF = cloudbaseinit_conf.CONF
|
||||||
LOG = oslo_logging.getLogger(__name__)
|
LOG = oslo_logging.getLogger(__name__)
|
||||||
|
EXPERIMENTAL_NOTICE = ("EXPERIMENTAL: The structure and format of content "
|
||||||
|
"scoped under the 'ds' key may change in subsequent "
|
||||||
|
"releases of cloud-init.")
|
||||||
|
|
||||||
|
|
||||||
class NotExistingMetadataException(Exception):
|
class NotExistingMetadataException(Exception):
|
||||||
@ -236,28 +239,52 @@ class BaseMetadataService(object, metaclass=abc.ABCMeta):
|
|||||||
The ds namespace can change without prior notice and should not be
|
The ds namespace can change without prior notice and should not be
|
||||||
used in production.
|
used in production.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
instance_id = self.get_instance_id()
|
instance_id = self.get_instance_id()
|
||||||
hostname = self.get_host_name()
|
hostname = self.get_host_name()
|
||||||
|
|
||||||
v1_data = {
|
v1_data = {
|
||||||
|
"instance-id": instance_id,
|
||||||
"instance_id": instance_id,
|
"instance_id": instance_id,
|
||||||
|
"local-hostname": hostname,
|
||||||
"local_hostname": hostname,
|
"local_hostname": hostname,
|
||||||
"public_ssh_keys": self.get_public_keys()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Copy the v1 data to the ds.meta_data and add more fields
|
# Copy the v1 data to the ds.meta_data and add more fields
|
||||||
|
ds_meta_data = self._get_datasource_instance_meta_data()
|
||||||
|
if not ds_meta_data:
|
||||||
ds_meta_data = copy.deepcopy(v1_data)
|
ds_meta_data = copy.deepcopy(v1_data)
|
||||||
ds_meta_data.update({
|
ds_meta_data.update({
|
||||||
"hostname": hostname
|
"hostname": hostname
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
v1_data["public_ssh_keys"] = self.get_public_keys()
|
||||||
|
md = {
|
||||||
"v1": v1_data,
|
"v1": v1_data,
|
||||||
"ds": {
|
"ds": {
|
||||||
|
"_doc": EXPERIMENTAL_NOTICE,
|
||||||
"meta_data": ds_meta_data,
|
"meta_data": ds_meta_data,
|
||||||
|
},
|
||||||
|
"instance-id": instance_id,
|
||||||
|
"instance_id": instance_id,
|
||||||
|
"local-hostname": hostname,
|
||||||
|
"local_hostname": hostname,
|
||||||
|
"public_ssh_keys": self.get_public_keys()
|
||||||
}
|
}
|
||||||
}
|
return md
|
||||||
|
|
||||||
|
def _get_datasource_instance_meta_data(self):
|
||||||
|
"""Returns a dictionary with datasource specific instance data
|
||||||
|
|
||||||
|
The instance data structure is based on the cloud-init specifications:
|
||||||
|
https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html
|
||||||
|
|
||||||
|
Datasource-specific metadata crawled for the specific cloud platform.
|
||||||
|
It should closely represent the structure of the cloud metadata
|
||||||
|
crawled. The structure of content and details provided are entirely
|
||||||
|
cloud-dependent.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseHTTPMetadataService(BaseMetadataService):
|
class BaseHTTPMetadataService(BaseMetadataService):
|
||||||
|
@ -30,6 +30,9 @@ from cloudbaseinit.utils import serialization
|
|||||||
CONF = cloudbaseinit_conf.CONF
|
CONF = cloudbaseinit_conf.CONF
|
||||||
LOG = oslo_logging.getLogger(__name__)
|
LOG = oslo_logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_GATEWAY_CIDR_IPV4 = u"0.0.0.0/0"
|
||||||
|
DEFAULT_GATEWAY_CIDR_IPV6 = u"::/0"
|
||||||
|
|
||||||
|
|
||||||
class NoCloudNetworkConfigV1Parser(object):
|
class NoCloudNetworkConfigV1Parser(object):
|
||||||
NETWORK_LINK_TYPE_PHY = 'physical'
|
NETWORK_LINK_TYPE_PHY = 'physical'
|
||||||
@ -280,9 +283,6 @@ class NoCloudNetworkConfigV1Parser(object):
|
|||||||
|
|
||||||
|
|
||||||
class NoCloudNetworkConfigV2Parser(object):
|
class NoCloudNetworkConfigV2Parser(object):
|
||||||
DEFAULT_GATEWAY_CIDR_IPV4 = u"0.0.0.0/0"
|
|
||||||
DEFAULT_GATEWAY_CIDR_IPV6 = u"::/0"
|
|
||||||
|
|
||||||
NETWORK_LINK_TYPE_ETHERNET = 'ethernet'
|
NETWORK_LINK_TYPE_ETHERNET = 'ethernet'
|
||||||
NETWORK_LINK_TYPE_BOND = 'bond'
|
NETWORK_LINK_TYPE_BOND = 'bond'
|
||||||
NETWORK_LINK_TYPE_VLAN = 'vlan'
|
NETWORK_LINK_TYPE_VLAN = 'vlan'
|
||||||
@ -308,11 +308,11 @@ class NoCloudNetworkConfigV2Parser(object):
|
|||||||
default_route = None
|
default_route = None
|
||||||
if gateway6 and netaddr.valid_ipv6(gateway6):
|
if gateway6 and netaddr.valid_ipv6(gateway6):
|
||||||
default_route = network_model.Route(
|
default_route = network_model.Route(
|
||||||
network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV6,
|
network_cidr=DEFAULT_GATEWAY_CIDR_IPV6,
|
||||||
gateway=gateway6)
|
gateway=gateway6)
|
||||||
elif gateway4 and netaddr.valid_ipv4(gateway4):
|
elif gateway4 and netaddr.valid_ipv4(gateway4):
|
||||||
default_route = network_model.Route(
|
default_route = network_model.Route(
|
||||||
network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV4,
|
network_cidr=DEFAULT_GATEWAY_CIDR_IPV4,
|
||||||
gateway=gateway4)
|
gateway=gateway4)
|
||||||
if default_route:
|
if default_route:
|
||||||
routes.append(default_route)
|
routes.append(default_route)
|
||||||
@ -324,9 +324,9 @@ class NoCloudNetworkConfigV2Parser(object):
|
|||||||
gateway = route_config.get("via")
|
gateway = route_config.get("via")
|
||||||
if network_cidr.lower() == "default":
|
if network_cidr.lower() == "default":
|
||||||
if netaddr.valid_ipv6(gateway):
|
if netaddr.valid_ipv6(gateway):
|
||||||
network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV6
|
network_cidr = DEFAULT_GATEWAY_CIDR_IPV6
|
||||||
else:
|
else:
|
||||||
network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV4
|
network_cidr = DEFAULT_GATEWAY_CIDR_IPV4
|
||||||
route = network_model.Route(
|
route = network_model.Route(
|
||||||
network_cidr=network_cidr,
|
network_cidr=network_cidr,
|
||||||
gateway=gateway)
|
gateway=gateway)
|
||||||
@ -547,6 +547,113 @@ class NoCloudNetworkConfigParser(object):
|
|||||||
|
|
||||||
return network_config_parser.parse(network_data)
|
return network_config_parser.parse(network_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def network_details_v1_to_v2(v1_networks):
|
||||||
|
"""Converts `NetworkDetails` objects to `NetworkDetailsV2` object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not v1_networks:
|
||||||
|
return None
|
||||||
|
|
||||||
|
links = []
|
||||||
|
networks = []
|
||||||
|
services = []
|
||||||
|
for nic in v1_networks:
|
||||||
|
link = network_model.Link(
|
||||||
|
id=nic.name,
|
||||||
|
name=nic.name,
|
||||||
|
type=network_model.LINK_TYPE_PHYSICAL,
|
||||||
|
mac_address=nic.mac,
|
||||||
|
enabled=None,
|
||||||
|
mtu=None,
|
||||||
|
bond=None,
|
||||||
|
vlan_link=None,
|
||||||
|
vlan_id=None,
|
||||||
|
)
|
||||||
|
links.append(link)
|
||||||
|
|
||||||
|
dns_addresses_v4 = []
|
||||||
|
dns_addresses_v6 = []
|
||||||
|
if nic.dnsnameservers:
|
||||||
|
for ns in nic.dnsnameservers:
|
||||||
|
if netaddr.valid_ipv6(ns):
|
||||||
|
dns_addresses_v6.append(ns)
|
||||||
|
else:
|
||||||
|
dns_addresses_v4.append(ns)
|
||||||
|
|
||||||
|
dns_services_v6 = None
|
||||||
|
if dns_addresses_v6:
|
||||||
|
dns_service_v6 = network_model.NameServerService(
|
||||||
|
addresses=dns_addresses_v6,
|
||||||
|
search=None,
|
||||||
|
)
|
||||||
|
dns_services_v6 = [dns_service_v6]
|
||||||
|
services.append(dns_service_v6)
|
||||||
|
|
||||||
|
dns_services_v4 = None
|
||||||
|
if dns_addresses_v4:
|
||||||
|
dns_service_v4 = network_model.NameServerService(
|
||||||
|
addresses=dns_addresses_v4,
|
||||||
|
search=None,
|
||||||
|
)
|
||||||
|
dns_services_v4 = [dns_service_v4]
|
||||||
|
services.append(dns_service_v4)
|
||||||
|
|
||||||
|
# Note: IPv6 address might be set to IPv4 field
|
||||||
|
# Not sure if it's a bug
|
||||||
|
default_route_v6 = None
|
||||||
|
default_route_v4 = None
|
||||||
|
if nic.gateway6:
|
||||||
|
default_route_v6 = network_model.Route(
|
||||||
|
network_cidr=DEFAULT_GATEWAY_CIDR_IPV6,
|
||||||
|
gateway=nic.gateway6)
|
||||||
|
|
||||||
|
if nic.gateway:
|
||||||
|
if netaddr.valid_ipv6(nic.gateway):
|
||||||
|
default_route_v6 = network_model.Route(
|
||||||
|
network_cidr=DEFAULT_GATEWAY_CIDR_IPV6,
|
||||||
|
gateway=nic.gateway)
|
||||||
|
else:
|
||||||
|
default_route_v4 = network_model.Route(
|
||||||
|
network_cidr=DEFAULT_GATEWAY_CIDR_IPV4,
|
||||||
|
gateway=nic.gateway)
|
||||||
|
|
||||||
|
routes_v6 = [default_route_v6] if default_route_v6 else []
|
||||||
|
routes_v4 = [default_route_v4] if default_route_v4 else []
|
||||||
|
|
||||||
|
if nic.address6:
|
||||||
|
net = network_model.Network(
|
||||||
|
link=link.name,
|
||||||
|
address_cidr=network_utils.ip_netmask_to_cidr(
|
||||||
|
nic.address6, nic.netmask6),
|
||||||
|
routes=routes_v6,
|
||||||
|
dns_nameservers=dns_services_v6,
|
||||||
|
)
|
||||||
|
networks.append(net)
|
||||||
|
|
||||||
|
if nic.address:
|
||||||
|
if netaddr.valid_ipv6(nic.address):
|
||||||
|
net = network_model.Network(
|
||||||
|
link=link.name,
|
||||||
|
address_cidr=network_utils.ip_netmask_to_cidr(
|
||||||
|
nic.address, nic.netmask),
|
||||||
|
routes=routes_v6,
|
||||||
|
dns_nameservers=dns_services_v6,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
net = network_model.Network(
|
||||||
|
link=link.name,
|
||||||
|
address_cidr=network_utils.ip_netmask_to_cidr(
|
||||||
|
nic.address, nic.netmask),
|
||||||
|
routes=routes_v4,
|
||||||
|
dns_nameservers=dns_services_v4,
|
||||||
|
)
|
||||||
|
networks.append(net)
|
||||||
|
|
||||||
|
return network_model.NetworkDetailsV2(links=links,
|
||||||
|
networks=networks,
|
||||||
|
services=services)
|
||||||
|
|
||||||
|
|
||||||
class NoCloudConfigDriveService(baseconfigdrive.BaseConfigDriveService):
|
class NoCloudConfigDriveService(baseconfigdrive.BaseConfigDriveService):
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ from cloudbaseinit import exception
|
|||||||
from cloudbaseinit.metadata.services import base
|
from cloudbaseinit.metadata.services import base
|
||||||
from cloudbaseinit.metadata.services import nocloudservice
|
from cloudbaseinit.metadata.services import nocloudservice
|
||||||
from cloudbaseinit.osutils import factory as osutils_factory
|
from cloudbaseinit.osutils import factory as osutils_factory
|
||||||
|
from cloudbaseinit.utils import network
|
||||||
from cloudbaseinit.utils import serialization
|
from cloudbaseinit.utils import serialization
|
||||||
|
|
||||||
CONF = cloudbaseinit_conf.CONF
|
CONF = cloudbaseinit_conf.CONF
|
||||||
@ -239,3 +240,22 @@ class VMwareGuestInfoService(base.BaseMetadataService):
|
|||||||
|
|
||||||
LOG.debug("network data %s", network)
|
LOG.debug("network data %s", network)
|
||||||
return {"network": network}
|
return {"network": network}
|
||||||
|
|
||||||
|
def _get_datasource_instance_meta_data(self):
|
||||||
|
"""Returns a dictionary with datasource specific instance data
|
||||||
|
|
||||||
|
The instance data structure is based on the cloud-init specifications:
|
||||||
|
https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html
|
||||||
|
|
||||||
|
Datasource-specific metadata crawled for the specific cloud platform.
|
||||||
|
It should closely represent the structure of the cloud metadata
|
||||||
|
crawled. The structure of content and details provided are entirely
|
||||||
|
cloud-dependent.
|
||||||
|
|
||||||
|
"""
|
||||||
|
ds = dict()
|
||||||
|
network_details = self.get_network_details_v2()
|
||||||
|
host_info = network.get_host_info(self.get_host_name(),
|
||||||
|
network_details)
|
||||||
|
ds.update(host_info)
|
||||||
|
return ds
|
||||||
|
@ -61,38 +61,63 @@ class TestBase(unittest.TestCase):
|
|||||||
result = self._service.get_user_pwd_encryption_key()
|
result = self._service.get_user_pwd_encryption_key()
|
||||||
self.assertEqual(result, mock_get_public_keys.return_value[0])
|
self.assertEqual(result, mock_get_public_keys.return_value[0])
|
||||||
|
|
||||||
|
@mock.patch('cloudbaseinit.metadata.services.base.'
|
||||||
|
'BaseMetadataService._get_datasource_instance_meta_data')
|
||||||
@mock.patch('cloudbaseinit.metadata.services.base.'
|
@mock.patch('cloudbaseinit.metadata.services.base.'
|
||||||
'BaseMetadataService.get_public_keys')
|
'BaseMetadataService.get_public_keys')
|
||||||
@mock.patch('cloudbaseinit.metadata.services.base.'
|
@mock.patch('cloudbaseinit.metadata.services.base.'
|
||||||
'BaseMetadataService.get_host_name')
|
'BaseMetadataService.get_host_name')
|
||||||
@mock.patch('cloudbaseinit.metadata.services.base.'
|
@mock.patch('cloudbaseinit.metadata.services.base.'
|
||||||
'BaseMetadataService.get_instance_id')
|
'BaseMetadataService.get_instance_id')
|
||||||
def test_get_instance_data(self, mock_instance_id, mock_hostname,
|
def _test_get_instance_data_with_datasource_meta_data(
|
||||||
mock_public_keys):
|
self, mock_instance_id, mock_hostname, mock_public_keys,
|
||||||
|
mock_get_datasource_instance_meta_data, datasource_meta_data=None):
|
||||||
fake_instance_id = 'id'
|
fake_instance_id = 'id'
|
||||||
mock_instance_id.return_value = fake_instance_id
|
mock_instance_id.return_value = fake_instance_id
|
||||||
fake_hostname = 'host'
|
fake_hostname = 'host'
|
||||||
mock_hostname.return_value = fake_hostname
|
mock_hostname.return_value = fake_hostname
|
||||||
fake_keys = ['ssh1', 'ssh2']
|
fake_keys = ['ssh1', 'ssh2']
|
||||||
mock_public_keys.return_value = fake_keys
|
mock_public_keys.return_value = fake_keys
|
||||||
|
mock_get_datasource_instance_meta_data.return_value = \
|
||||||
|
datasource_meta_data
|
||||||
|
|
||||||
|
if datasource_meta_data:
|
||||||
|
ds_md = datasource_meta_data
|
||||||
|
else:
|
||||||
|
ds_md = {
|
||||||
|
"instance_id": fake_instance_id,
|
||||||
|
"instance-id": fake_instance_id,
|
||||||
|
"local_hostname": fake_hostname,
|
||||||
|
"local-hostname": fake_hostname,
|
||||||
|
"hostname": fake_hostname
|
||||||
|
}
|
||||||
expected_response = {
|
expected_response = {
|
||||||
'v1': {
|
'v1': {
|
||||||
"instance_id": fake_instance_id,
|
"instance_id": fake_instance_id,
|
||||||
|
"instance-id": fake_instance_id,
|
||||||
"local_hostname": fake_hostname,
|
"local_hostname": fake_hostname,
|
||||||
|
"local-hostname": fake_hostname,
|
||||||
"public_ssh_keys": fake_keys
|
"public_ssh_keys": fake_keys
|
||||||
},
|
},
|
||||||
'ds': {
|
'ds': {
|
||||||
'meta_data': {
|
'_doc': base.EXPERIMENTAL_NOTICE,
|
||||||
"instance_id": fake_instance_id,
|
'meta_data': ds_md,
|
||||||
"local_hostname": fake_hostname,
|
|
||||||
"public_ssh_keys": fake_keys,
|
|
||||||
"hostname": fake_hostname
|
|
||||||
},
|
},
|
||||||
}
|
"instance_id": fake_instance_id,
|
||||||
|
"instance-id": fake_instance_id,
|
||||||
|
"local_hostname": fake_hostname,
|
||||||
|
"local-hostname": fake_hostname,
|
||||||
|
"public_ssh_keys": fake_keys,
|
||||||
}
|
}
|
||||||
self.assertEqual(expected_response, self._service.get_instance_data())
|
self.assertEqual(expected_response, self._service.get_instance_data())
|
||||||
|
|
||||||
|
def test_get_instance_data(self):
|
||||||
|
self._test_get_instance_data_with_datasource_meta_data()
|
||||||
|
|
||||||
|
def test_get_instance_data_with_datasource_meta_data(self):
|
||||||
|
self._test_get_instance_data_with_datasource_meta_data(
|
||||||
|
datasource_meta_data={'fake-data': 'fake-value'})
|
||||||
|
|
||||||
|
|
||||||
class TestBaseHTTPMetadataService(unittest.TestCase):
|
class TestBaseHTTPMetadataService(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -20,10 +20,16 @@ import unittest
|
|||||||
import unittest.mock as mock
|
import unittest.mock as mock
|
||||||
|
|
||||||
from cloudbaseinit.metadata.services import base
|
from cloudbaseinit.metadata.services import base
|
||||||
|
from cloudbaseinit.metadata.services.nocloudservice import \
|
||||||
|
NoCloudNetworkConfigParser
|
||||||
from cloudbaseinit.models import network as nm
|
from cloudbaseinit.models import network as nm
|
||||||
|
from cloudbaseinit.tests.metadata import fake_json_response
|
||||||
from cloudbaseinit.tests import testutils
|
from cloudbaseinit.tests import testutils
|
||||||
|
from cloudbaseinit.utils import debiface
|
||||||
|
from cloudbaseinit.utils import network
|
||||||
from cloudbaseinit.utils import serialization
|
from cloudbaseinit.utils import serialization
|
||||||
|
|
||||||
|
|
||||||
MODULE_PATH = "cloudbaseinit.metadata.services.nocloudservice"
|
MODULE_PATH = "cloudbaseinit.metadata.services.nocloudservice"
|
||||||
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_EMPTY_CONFIG = """
|
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_EMPTY_CONFIG = """
|
||||||
network:
|
network:
|
||||||
@ -531,3 +537,98 @@ class TestNoCloudConfigDriveService(unittest.TestCase):
|
|||||||
|
|
||||||
mock_get_cache_data.assert_called_with(
|
mock_get_cache_data.assert_called_with(
|
||||||
"network-config", decode=True)
|
"network-config", decode=True)
|
||||||
|
|
||||||
|
def test_to_network_details_v2(self):
|
||||||
|
date = "2013-04-04"
|
||||||
|
content = fake_json_response.get_fake_metadata_json(date)
|
||||||
|
nics = debiface.parse(content["network_config"]["debian_config"])
|
||||||
|
v2 = NoCloudNetworkConfigParser.network_details_v1_to_v2(nics)
|
||||||
|
link0 = nm.Link(
|
||||||
|
id=fake_json_response.NAME0,
|
||||||
|
name=fake_json_response.NAME0,
|
||||||
|
type=nm.LINK_TYPE_PHYSICAL,
|
||||||
|
mac_address=fake_json_response.MAC0.upper(),
|
||||||
|
enabled=None,
|
||||||
|
mtu=None,
|
||||||
|
bond=None,
|
||||||
|
vlan_link=None,
|
||||||
|
vlan_id=None,
|
||||||
|
)
|
||||||
|
link1 = nm.Link(
|
||||||
|
id=fake_json_response.NAME1,
|
||||||
|
name=fake_json_response.NAME1,
|
||||||
|
type=nm.LINK_TYPE_PHYSICAL,
|
||||||
|
mac_address=None,
|
||||||
|
enabled=None,
|
||||||
|
mtu=None,
|
||||||
|
bond=None,
|
||||||
|
vlan_link=None,
|
||||||
|
vlan_id=None,
|
||||||
|
)
|
||||||
|
link2 = nm.Link(
|
||||||
|
id=fake_json_response.NAME2,
|
||||||
|
name=fake_json_response.NAME2,
|
||||||
|
type=nm.LINK_TYPE_PHYSICAL,
|
||||||
|
mac_address=fake_json_response.MAC2,
|
||||||
|
enabled=None,
|
||||||
|
mtu=None,
|
||||||
|
bond=None,
|
||||||
|
vlan_link=None,
|
||||||
|
vlan_id=None,
|
||||||
|
)
|
||||||
|
dns_service0 = nm.NameServerService(
|
||||||
|
addresses=fake_json_response.DNSNS0.split(),
|
||||||
|
search=None,
|
||||||
|
)
|
||||||
|
network0 = nm.Network(
|
||||||
|
link=fake_json_response.NAME0,
|
||||||
|
address_cidr=network.ip_netmask_to_cidr(
|
||||||
|
fake_json_response.ADDRESS0, fake_json_response.NETMASK0),
|
||||||
|
routes=[nm.Route(
|
||||||
|
network_cidr=u"0.0.0.0/0",
|
||||||
|
gateway=fake_json_response.GATEWAY0,
|
||||||
|
)],
|
||||||
|
dns_nameservers=[dns_service0],
|
||||||
|
)
|
||||||
|
network0_v6 = nm.Network(
|
||||||
|
link=fake_json_response.NAME0,
|
||||||
|
address_cidr=network.ip_netmask_to_cidr(
|
||||||
|
fake_json_response.ADDRESS60, fake_json_response.NETMASK60),
|
||||||
|
routes=[nm.Route(
|
||||||
|
network_cidr=u"::/0",
|
||||||
|
gateway=fake_json_response.GATEWAY60,
|
||||||
|
)],
|
||||||
|
dns_nameservers=None,
|
||||||
|
)
|
||||||
|
network1 = nm.Network(
|
||||||
|
link=fake_json_response.NAME1,
|
||||||
|
address_cidr=network.ip_netmask_to_cidr(
|
||||||
|
fake_json_response.ADDRESS1, fake_json_response.NETMASK1),
|
||||||
|
routes=[nm.Route(
|
||||||
|
network_cidr=u"0.0.0.0/0",
|
||||||
|
gateway=fake_json_response.GATEWAY1,
|
||||||
|
)],
|
||||||
|
dns_nameservers=None,
|
||||||
|
)
|
||||||
|
network2 = nm.Network(
|
||||||
|
link=fake_json_response.NAME2,
|
||||||
|
address_cidr=network.ip_netmask_to_cidr(
|
||||||
|
fake_json_response.ADDRESS2, fake_json_response.NETMASK2),
|
||||||
|
routes=[nm.Route(
|
||||||
|
network_cidr=u"::/0",
|
||||||
|
gateway=fake_json_response.GATEWAY2,
|
||||||
|
)],
|
||||||
|
dns_nameservers=None,
|
||||||
|
)
|
||||||
|
expected = nm.NetworkDetailsV2(
|
||||||
|
links=[
|
||||||
|
link0, link1, link2,
|
||||||
|
],
|
||||||
|
networks=[
|
||||||
|
network0_v6, network0, network1, network2,
|
||||||
|
],
|
||||||
|
services=[
|
||||||
|
dns_service0
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(expected, v2)
|
||||||
|
@ -292,3 +292,10 @@ class VMwareGuestInfoServiceTest(unittest.TestCase):
|
|||||||
mock_get_guestinfo_value.return_value = "no encoding"
|
mock_get_guestinfo_value.return_value = "no encoding"
|
||||||
self.assertRaises(exception.CloudbaseInitException,
|
self.assertRaises(exception.CloudbaseInitException,
|
||||||
self._service._get_guest_data, 'fake_key')
|
self._service._get_guest_data, 'fake_key')
|
||||||
|
|
||||||
|
@mock.patch("cloudbaseinit.utils.network.get_host_info")
|
||||||
|
def test_get_datasource_instance_meta_data(self, mock_get_host_info):
|
||||||
|
expect_md = {'network': 'fake_host_info'}
|
||||||
|
mock_get_host_info.return_value = expect_md
|
||||||
|
self.assertEqual(expect_md,
|
||||||
|
self._service._get_datasource_instance_meta_data())
|
||||||
|
@ -12,10 +12,12 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
import unittest
|
import unittest
|
||||||
import unittest.mock as mock
|
import unittest.mock as mock
|
||||||
|
|
||||||
from cloudbaseinit import conf as cloudbaseinit_conf
|
from cloudbaseinit import conf as cloudbaseinit_conf
|
||||||
|
from cloudbaseinit.models import network as network_model
|
||||||
from cloudbaseinit.tests import testutils
|
from cloudbaseinit.tests import testutils
|
||||||
from cloudbaseinit.utils import network
|
from cloudbaseinit.utils import network
|
||||||
|
|
||||||
@ -24,6 +26,89 @@ CONF = cloudbaseinit_conf.CONF
|
|||||||
|
|
||||||
|
|
||||||
class NetworkUtilsTest(unittest.TestCase):
|
class NetworkUtilsTest(unittest.TestCase):
|
||||||
|
link0 = network_model.Link(
|
||||||
|
id="eth0",
|
||||||
|
name="eth0",
|
||||||
|
type=network_model.LINK_TYPE_PHYSICAL,
|
||||||
|
mac_address="ab:cd:ef:ef:cd:ab",
|
||||||
|
enabled=None,
|
||||||
|
mtu=None,
|
||||||
|
bond=None,
|
||||||
|
vlan_link=None,
|
||||||
|
vlan_id=None,
|
||||||
|
)
|
||||||
|
network_private_default_route = network_model.Network(
|
||||||
|
link="eth0",
|
||||||
|
address_cidr="192.168.1.2/24",
|
||||||
|
routes=[
|
||||||
|
network_model.Route(
|
||||||
|
network_cidr=u"0.0.0.0/0",
|
||||||
|
gateway="192.168.1.1",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
dns_nameservers=[]
|
||||||
|
)
|
||||||
|
network_public = network_model.Network(
|
||||||
|
link="eth0",
|
||||||
|
address_cidr="2.3.4.2/24",
|
||||||
|
routes=[
|
||||||
|
network_model.Route(
|
||||||
|
network_cidr=u"2.3.4.1/24",
|
||||||
|
gateway="2.3.4.1",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
dns_nameservers=[]
|
||||||
|
)
|
||||||
|
network_public_default_route = network_model.Network(
|
||||||
|
link="eth0",
|
||||||
|
address_cidr="2.3.4.2/24",
|
||||||
|
routes=[
|
||||||
|
network_model.Route(
|
||||||
|
network_cidr=u"0.0.0.0/0",
|
||||||
|
gateway="2.3.4.1",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
dns_nameservers=[]
|
||||||
|
)
|
||||||
|
network_private = network_model.Network(
|
||||||
|
link="eth0",
|
||||||
|
address_cidr="172.10.1.2/24",
|
||||||
|
routes=[
|
||||||
|
network_model.Route(
|
||||||
|
network_cidr=u"172.10.1.1/24",
|
||||||
|
gateway="172.10.1.1",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
dns_nameservers=[]
|
||||||
|
)
|
||||||
|
network_local = network_model.Network(
|
||||||
|
link="eth0",
|
||||||
|
address_cidr="127.0.0.4/24",
|
||||||
|
routes=[
|
||||||
|
network_model.Route(
|
||||||
|
network_cidr=u"127.0.0.4/24",
|
||||||
|
gateway="127.0.0.2",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
dns_nameservers=[]
|
||||||
|
)
|
||||||
|
ipv6_addr = '1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38'
|
||||||
|
network_v6 = network_model.Network(
|
||||||
|
link="eth0",
|
||||||
|
address_cidr=ipv6_addr + '/64',
|
||||||
|
routes=[network_model.Route(
|
||||||
|
network_cidr=u"::/0",
|
||||||
|
gateway="::1",
|
||||||
|
)],
|
||||||
|
dns_nameservers=[]
|
||||||
|
)
|
||||||
|
ipv6_addr_private = 'fe80::216:3eff:fe16:db54'
|
||||||
|
network_v6_local = network_model.Network(
|
||||||
|
link="eth0",
|
||||||
|
address_cidr=ipv6_addr_private + '/64',
|
||||||
|
routes=[],
|
||||||
|
dns_nameservers=[]
|
||||||
|
)
|
||||||
|
|
||||||
@mock.patch('urllib.request.urlopen')
|
@mock.patch('urllib.request.urlopen')
|
||||||
def test_check_url(self, mock_url_open):
|
def test_check_url(self, mock_url_open):
|
||||||
@ -110,3 +195,152 @@ class NetworkUtilsTest(unittest.TestCase):
|
|||||||
fake_netmask = None
|
fake_netmask = None
|
||||||
self._test_ip_netmask_to_cidr(fake_ip_address, fake_ip_address,
|
self._test_ip_netmask_to_cidr(fake_ip_address, fake_ip_address,
|
||||||
fake_netmask)
|
fake_netmask)
|
||||||
|
|
||||||
|
def test_get_default_ip_addresses(self):
|
||||||
|
network_details = network_model.NetworkDetailsV2(
|
||||||
|
links=[
|
||||||
|
self.link0
|
||||||
|
],
|
||||||
|
networks=[
|
||||||
|
self.network_private_default_route,
|
||||||
|
],
|
||||||
|
services=[
|
||||||
|
],
|
||||||
|
)
|
||||||
|
ipv4, ipv6 = network.get_default_ip_addresses(network_details)
|
||||||
|
self.assertEqual('192.168.1.2', ipv4)
|
||||||
|
self.assertIsNone(ipv6)
|
||||||
|
|
||||||
|
def test_get_default_ip_addresses_link_local(self):
|
||||||
|
network_details = network_model.NetworkDetailsV2(
|
||||||
|
links=[
|
||||||
|
self.link0
|
||||||
|
],
|
||||||
|
networks=[
|
||||||
|
self.network_private,
|
||||||
|
],
|
||||||
|
services=[
|
||||||
|
],
|
||||||
|
)
|
||||||
|
ipv4, ipv6 = network.get_default_ip_addresses(network_details)
|
||||||
|
self.assertIsNone(ipv4)
|
||||||
|
self.assertIsNone(ipv6)
|
||||||
|
|
||||||
|
def test_get_default_ip_addresses_public_default_route(self):
|
||||||
|
network_details = network_model.NetworkDetailsV2(
|
||||||
|
links=[
|
||||||
|
self.link0
|
||||||
|
],
|
||||||
|
networks=[
|
||||||
|
self.network_public_default_route,
|
||||||
|
],
|
||||||
|
services=[
|
||||||
|
],
|
||||||
|
)
|
||||||
|
ipv4, ipv6 = network.get_default_ip_addresses(network_details)
|
||||||
|
self.assertEqual("2.3.4.2", ipv4)
|
||||||
|
self.assertIsNone(ipv6)
|
||||||
|
|
||||||
|
def test_get_default_ip_addresses_v6(self):
|
||||||
|
network_details = network_model.NetworkDetailsV2(
|
||||||
|
links=[
|
||||||
|
self.link0
|
||||||
|
],
|
||||||
|
networks=[
|
||||||
|
self.network_v6,
|
||||||
|
],
|
||||||
|
services=[
|
||||||
|
],
|
||||||
|
)
|
||||||
|
ipv4, ipv6 = network.get_default_ip_addresses(network_details)
|
||||||
|
self.assertIsNone(ipv4)
|
||||||
|
self.assertEqual(self.ipv6_addr, ipv6)
|
||||||
|
|
||||||
|
def test_get_default_ip_addresses_v6_local(self):
|
||||||
|
network_details = network_model.NetworkDetailsV2(
|
||||||
|
links=[
|
||||||
|
self.link0
|
||||||
|
],
|
||||||
|
networks=[
|
||||||
|
self.network_v6_local,
|
||||||
|
],
|
||||||
|
services=[
|
||||||
|
],
|
||||||
|
)
|
||||||
|
ipv4, ipv6 = network.get_default_ip_addresses(network_details)
|
||||||
|
self.assertIsNone(ipv4)
|
||||||
|
self.assertIsNone(ipv6)
|
||||||
|
|
||||||
|
def test_get_default_ip_addresses_dual_stack(self):
|
||||||
|
network_details = network_model.NetworkDetailsV2(
|
||||||
|
links=[
|
||||||
|
self.link0
|
||||||
|
],
|
||||||
|
networks=[
|
||||||
|
self.network_private_default_route,
|
||||||
|
self.network_public,
|
||||||
|
self.network_v6,
|
||||||
|
],
|
||||||
|
services=[
|
||||||
|
],
|
||||||
|
)
|
||||||
|
ipv4, ipv6 = network.get_default_ip_addresses(network_details)
|
||||||
|
self.assertEqual('192.168.1.2', ipv4)
|
||||||
|
self.assertEqual(self.ipv6_addr, ipv6)
|
||||||
|
|
||||||
|
def test_get_host_info(self):
|
||||||
|
network_details = network_model.NetworkDetailsV2(
|
||||||
|
links=[
|
||||||
|
self.link0
|
||||||
|
],
|
||||||
|
networks=[
|
||||||
|
self.network_private_default_route,
|
||||||
|
self.network_public, self.network_v6,
|
||||||
|
],
|
||||||
|
services=[
|
||||||
|
],
|
||||||
|
)
|
||||||
|
expect = {
|
||||||
|
'hostname': 'fake_host',
|
||||||
|
'local-hostname': 'fake_host',
|
||||||
|
'local-ipv4': '192.168.1.2',
|
||||||
|
'local_ipv4': '192.168.1.2',
|
||||||
|
'local-ipv6': '1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38',
|
||||||
|
'local_ipv6': '1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38',
|
||||||
|
'local_hostname': 'fake_host',
|
||||||
|
'network': {'interfaces': {
|
||||||
|
'by-ipv4': collections.OrderedDict([
|
||||||
|
('192.168.1.2',
|
||||||
|
{'broadcast': '192.168.1.255',
|
||||||
|
'mac': 'ab:cd:ef:ef:cd:ab',
|
||||||
|
'netmask': '255.255.255.0'}
|
||||||
|
),
|
||||||
|
('2.3.4.2',
|
||||||
|
{'broadcast': '2.3.4.255',
|
||||||
|
'mac': 'ab:cd:ef:ef:cd:ab',
|
||||||
|
'netmask': '255.255.255.0'}
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
'by-ipv6': collections.OrderedDict([
|
||||||
|
('1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38',
|
||||||
|
{'broadcast': '1a8f:9aaf:2904:858f:ffff:ffff:ffff:ffff',
|
||||||
|
'mac': 'ab:cd:ef:ef:cd:ab',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
'by-mac': collections.OrderedDict([
|
||||||
|
('ab:cd:ef:ef:cd:ab',
|
||||||
|
{'ipv4': {'addr': '2.3.4.2',
|
||||||
|
'broadcast': '2.3.4.255',
|
||||||
|
'netmask': '255.255.255.0'},
|
||||||
|
'ipv6': {
|
||||||
|
'addr': '1a8f:9aaf:2904:858f:1bce:6f85:2b04:f38',
|
||||||
|
'broadcast': '1a8f:9aaf:2904:858f:'
|
||||||
|
'ffff:ffff:ffff:ffff'}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
host_info = network.get_host_info('fake_host', network_details)
|
||||||
|
self.assertEqual(expect, host_info)
|
||||||
|
@ -14,6 +14,9 @@
|
|||||||
|
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
|
import collections
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
@ -28,6 +31,10 @@ from cloudbaseinit.osutils import factory as osutils_factory
|
|||||||
|
|
||||||
LOG = oslo_logging.getLogger(__name__)
|
LOG = oslo_logging.getLogger(__name__)
|
||||||
MAX_URL_CHECK_RETRIES = 3
|
MAX_URL_CHECK_RETRIES = 3
|
||||||
|
DEFAULT_GATEWAY_CIDR_IPV4 = u"0.0.0.0/0"
|
||||||
|
DEFAULT_GATEWAY_CIDR_IPV6 = u"::/0"
|
||||||
|
LOCAL_IPV4 = "local-ipv4"
|
||||||
|
LOCAL_IPV6 = "local-ipv6"
|
||||||
|
|
||||||
|
|
||||||
def get_local_ip(address=None):
|
def get_local_ip(address=None):
|
||||||
@ -98,3 +105,135 @@ def ip_netmask_to_cidr(ip_address, netmask):
|
|||||||
prefix_len = netaddr.IPNetwork(
|
prefix_len = netaddr.IPNetwork(
|
||||||
u"%s/%s" % (ip_address, netmask)).prefixlen
|
u"%s/%s" % (ip_address, netmask)).prefixlen
|
||||||
return u"%s/%s" % (ip_address, prefix_len)
|
return u"%s/%s" % (ip_address, prefix_len)
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_ip_addresses(network_details):
|
||||||
|
ipv4_address = None
|
||||||
|
ipv6_address = None
|
||||||
|
if network_details:
|
||||||
|
for net in network_details.networks:
|
||||||
|
ip_net = netaddr.IPNetwork(net.address_cidr)
|
||||||
|
addr = ip_net.ip
|
||||||
|
default_route = False
|
||||||
|
for route in net.routes:
|
||||||
|
if addr.version == 6 and \
|
||||||
|
route.network_cidr == DEFAULT_GATEWAY_CIDR_IPV6:
|
||||||
|
default_route = True
|
||||||
|
|
||||||
|
elif addr.version == 4 and \
|
||||||
|
route.network_cidr == DEFAULT_GATEWAY_CIDR_IPV4:
|
||||||
|
default_route = True
|
||||||
|
|
||||||
|
if not default_route:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not ipv6_address and addr.version == 6:
|
||||||
|
v6_addr = ipaddress.IPv6Address(addr)
|
||||||
|
if v6_addr.is_private or v6_addr.is_global:
|
||||||
|
ipv6_address = str(v6_addr)
|
||||||
|
|
||||||
|
if not ipv4_address and addr.version == 4:
|
||||||
|
v4_addr = ipaddress.IPv4Address(addr)
|
||||||
|
if v4_addr.is_private or v4_addr.is_global:
|
||||||
|
ipv4_address = str(v4_addr)
|
||||||
|
|
||||||
|
return ipv4_address, ipv6_address
|
||||||
|
|
||||||
|
|
||||||
|
def get_host_info(hostname, network_details):
|
||||||
|
"""Returns host information such as the host name and network interfaces.
|
||||||
|
|
||||||
|
"""
|
||||||
|
host_info = {
|
||||||
|
"network": {
|
||||||
|
"interfaces": {
|
||||||
|
"by-mac": collections.OrderedDict(),
|
||||||
|
"by-ipv4": collections.OrderedDict(),
|
||||||
|
"by-ipv6": collections.OrderedDict(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if hostname:
|
||||||
|
host_info["hostname"] = hostname
|
||||||
|
host_info["local-hostname"] = hostname
|
||||||
|
host_info["local_hostname"] = hostname
|
||||||
|
|
||||||
|
by_mac = host_info["network"]["interfaces"]["by-mac"]
|
||||||
|
by_ipv4 = host_info["network"]["interfaces"]["by-ipv4"]
|
||||||
|
by_ipv6 = host_info["network"]["interfaces"]["by-ipv6"]
|
||||||
|
|
||||||
|
if not network_details:
|
||||||
|
return host_info
|
||||||
|
|
||||||
|
default_ipv4, default_ipv6 = get_default_ip_addresses(network_details)
|
||||||
|
if default_ipv4:
|
||||||
|
host_info[LOCAL_IPV4] = default_ipv4
|
||||||
|
host_info[LOCAL_IPV4.replace('-', '_')] = default_ipv4
|
||||||
|
if default_ipv6:
|
||||||
|
host_info[LOCAL_IPV6] = default_ipv6
|
||||||
|
host_info[LOCAL_IPV6.replace('-', '_')] = default_ipv6
|
||||||
|
|
||||||
|
"""
|
||||||
|
IPv4: {
|
||||||
|
'bcast': '',
|
||||||
|
'ip': '127.0.0.1',
|
||||||
|
'mask': '255.0.0.0',
|
||||||
|
'scope': 'host',
|
||||||
|
}
|
||||||
|
IPv6: {
|
||||||
|
'ip': '::1/128',
|
||||||
|
'scope6': 'host'
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
mac_by_link_names = {}
|
||||||
|
for link in network_details.links:
|
||||||
|
mac_by_link_names[link.name] = link.mac_address
|
||||||
|
|
||||||
|
for net in network_details.networks:
|
||||||
|
mac = mac_by_link_names[net.link]
|
||||||
|
|
||||||
|
# Do not bother recording localhost
|
||||||
|
if mac == "00:00:00:00:00:00":
|
||||||
|
continue
|
||||||
|
|
||||||
|
ip_net = netaddr.IPNetwork(net.address_cidr)
|
||||||
|
addr = ip_net.ip
|
||||||
|
is_v6 = addr.version == 6
|
||||||
|
is_v4 = addr.version == 4
|
||||||
|
|
||||||
|
if mac:
|
||||||
|
if mac not in by_mac:
|
||||||
|
val = {}
|
||||||
|
else:
|
||||||
|
val = by_mac[mac]
|
||||||
|
key = None
|
||||||
|
if is_v4:
|
||||||
|
key = 'ipv4'
|
||||||
|
val[key] = {
|
||||||
|
'addr': str(addr),
|
||||||
|
'netmask': str(ip_net.netmask),
|
||||||
|
'broadcast': str(ip_net.broadcast),
|
||||||
|
}
|
||||||
|
elif is_v6:
|
||||||
|
key = 'ipv6'
|
||||||
|
val[key] = {
|
||||||
|
'addr': str(addr),
|
||||||
|
'broadcast': str(ip_net.broadcast),
|
||||||
|
}
|
||||||
|
if key:
|
||||||
|
by_mac[mac] = val
|
||||||
|
|
||||||
|
if is_v4:
|
||||||
|
by_ipv4[str(addr)] = {
|
||||||
|
'mac': mac,
|
||||||
|
'netmask': str(ip_net.netmask),
|
||||||
|
'broadcast': str(ip_net.broadcast),
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_v6:
|
||||||
|
by_ipv6[str(addr)] = {
|
||||||
|
'mac': mac,
|
||||||
|
'broadcast': str(ip_net.broadcast),
|
||||||
|
}
|
||||||
|
|
||||||
|
return host_info
|
||||||
|
@ -99,6 +99,9 @@ The following cloud-config directives are supported:
|
|||||||
|
|
||||||
1. path - Absolute path on disk where the content should be written.
|
1. path - Absolute path on disk where the content should be written.
|
||||||
2. content - The content which will be written in the given file.
|
2. content - The content which will be written in the given file.
|
||||||
|
Instance metadata can be used as template variables.
|
||||||
|
You may refer to https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html
|
||||||
|
for the example of instance data.
|
||||||
3. permissions - Integer representing file permissions.
|
3. permissions - Integer representing file permissions.
|
||||||
4. encoding - The encoding of the data in content. Supported encodings
|
4. encoding - The encoding of the data in content. Supported encodings
|
||||||
are: b64, base64 for base64-encoded content, gz,
|
are: b64, base64 for base64-encoded content, gz,
|
||||||
@ -133,6 +136,11 @@ The following cloud-config directives are supported:
|
|||||||
H4sIAGUfoFQC/zMxAgCIsCQyAgAAAA==
|
H4sIAGUfoFQC/zMxAgCIsCQyAgAAAA==
|
||||||
path: C:\gzip
|
path: C:\gzip
|
||||||
permissions: '0644'
|
permissions: '0644'
|
||||||
|
- path: C:\run\node-config.yaml
|
||||||
|
permissions: '0640'
|
||||||
|
content: |
|
||||||
|
---
|
||||||
|
node_ip: '{{ ds.meta_data.local_ipv4 }}'
|
||||||
|
|
||||||
* set_timezone - Change the underlying timezone.
|
* set_timezone - Change the underlying timezone.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user