Skip to content

Commit dc03674

Browse files
committed
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
1 parent c10ff68 commit dc03674

File tree

9 files changed

+693
-25
lines changed

9 files changed

+693
-25
lines changed

cloudbaseinit/metadata/services/base.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
CONF = cloudbaseinit_conf.CONF
3030
LOG = oslo_logging.getLogger(__name__)
31+
EXPERIMENTAL_NOTICE = ("EXPERIMENTAL: The structure and format of content "
32+
"scoped under the 'ds' key may change in subsequent "
33+
"releases of cloud-init.")
3134

3235

3336
class NotExistingMetadataException(Exception):
@@ -236,28 +239,52 @@ def get_instance_data(self):
236239
The ds namespace can change without prior notice and should not be
237240
used in production.
238241
"""
239-
240242
instance_id = self.get_instance_id()
241243
hostname = self.get_host_name()
242244

243245
v1_data = {
246+
"instance-id": instance_id,
244247
"instance_id": instance_id,
248+
"local-hostname": hostname,
245249
"local_hostname": hostname,
246-
"public_ssh_keys": self.get_public_keys()
247250
}
248251

249252
# Copy the v1 data to the ds.meta_data and add more fields
250-
ds_meta_data = copy.deepcopy(v1_data)
251-
ds_meta_data.update({
252-
"hostname": hostname
253-
})
254-
255-
return {
253+
ds_meta_data = self._get_datasource_instance_meta_data()
254+
if not ds_meta_data:
255+
ds_meta_data = copy.deepcopy(v1_data)
256+
ds_meta_data.update({
257+
"hostname": hostname
258+
})
259+
260+
v1_data["public_ssh_keys"] = self.get_public_keys()
261+
md = {
256262
"v1": v1_data,
257263
"ds": {
264+
"_doc": EXPERIMENTAL_NOTICE,
258265
"meta_data": ds_meta_data,
259-
}
266+
},
267+
"instance-id": instance_id,
268+
"instance_id": instance_id,
269+
"local-hostname": hostname,
270+
"local_hostname": hostname,
271+
"public_ssh_keys": self.get_public_keys()
260272
}
273+
return md
274+
275+
def _get_datasource_instance_meta_data(self):
276+
"""Returns a dictionary with datasource specific instance data
277+
278+
The instance data structure is based on the cloud-init specifications:
279+
https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html
280+
281+
Datasource-specific metadata crawled for the specific cloud platform.
282+
It should closely represent the structure of the cloud metadata
283+
crawled. The structure of content and details provided are entirely
284+
cloud-dependent.
285+
286+
"""
287+
pass
261288

262289

263290
class BaseHTTPMetadataService(BaseMetadataService):

cloudbaseinit/metadata/services/nocloudservice.py

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
CONF = cloudbaseinit_conf.CONF
3131
LOG = oslo_logging.getLogger(__name__)
3232

33+
DEFAULT_GATEWAY_CIDR_IPV4 = u"0.0.0.0/0"
34+
DEFAULT_GATEWAY_CIDR_IPV6 = u"::/0"
35+
3336

3437
class NoCloudNetworkConfigV1Parser(object):
3538
NETWORK_LINK_TYPE_PHY = 'physical'
@@ -280,9 +283,6 @@ def parse(self, network_config):
280283

281284

282285
class NoCloudNetworkConfigV2Parser(object):
283-
DEFAULT_GATEWAY_CIDR_IPV4 = u"0.0.0.0/0"
284-
DEFAULT_GATEWAY_CIDR_IPV6 = u"::/0"
285-
286286
NETWORK_LINK_TYPE_ETHERNET = 'ethernet'
287287
NETWORK_LINK_TYPE_BOND = 'bond'
288288
NETWORK_LINK_TYPE_VLAN = 'vlan'
@@ -308,11 +308,11 @@ def _parse_addresses(self, item, link_name):
308308
default_route = None
309309
if gateway6 and netaddr.valid_ipv6(gateway6):
310310
default_route = network_model.Route(
311-
network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV6,
311+
network_cidr=DEFAULT_GATEWAY_CIDR_IPV6,
312312
gateway=gateway6)
313313
elif gateway4 and netaddr.valid_ipv4(gateway4):
314314
default_route = network_model.Route(
315-
network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV4,
315+
network_cidr=DEFAULT_GATEWAY_CIDR_IPV4,
316316
gateway=gateway4)
317317
if default_route:
318318
routes.append(default_route)
@@ -324,9 +324,9 @@ def _parse_addresses(self, item, link_name):
324324
gateway = route_config.get("via")
325325
if network_cidr.lower() == "default":
326326
if netaddr.valid_ipv6(gateway):
327-
network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV6
327+
network_cidr = DEFAULT_GATEWAY_CIDR_IPV6
328328
else:
329-
network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV4
329+
network_cidr = DEFAULT_GATEWAY_CIDR_IPV4
330330
route = network_model.Route(
331331
network_cidr=network_cidr,
332332
gateway=gateway)
@@ -547,6 +547,113 @@ def parse(network_data):
547547

548548
return network_config_parser.parse(network_data)
549549

550+
@staticmethod
551+
def network_details_v1_to_v2(v1_networks):
552+
"""Converts `NetworkDetails` objects to `NetworkDetailsV2` object.
553+
554+
"""
555+
if not v1_networks:
556+
return None
557+
558+
links = []
559+
networks = []
560+
services = []
561+
for nic in v1_networks:
562+
link = network_model.Link(
563+
id=nic.name,
564+
name=nic.name,
565+
type=network_model.LINK_TYPE_PHYSICAL,
566+
mac_address=nic.mac,
567+
enabled=None,
568+
mtu=None,
569+
bond=None,
570+
vlan_link=None,
571+
vlan_id=None,
572+
)
573+
links.append(link)
574+
575+
dns_addresses_v4 = []
576+
dns_addresses_v6 = []
577+
if nic.dnsnameservers:
578+
for ns in nic.dnsnameservers:
579+
if netaddr.valid_ipv6(ns):
580+
dns_addresses_v6.append(ns)
581+
else:
582+
dns_addresses_v4.append(ns)
583+
584+
dns_services_v6 = None
585+
if dns_addresses_v6:
586+
dns_service_v6 = network_model.NameServerService(
587+
addresses=dns_addresses_v6,
588+
search=None,
589+
)
590+
dns_services_v6 = [dns_service_v6]
591+
services.append(dns_service_v6)
592+
593+
dns_services_v4 = None
594+
if dns_addresses_v4:
595+
dns_service_v4 = network_model.NameServerService(
596+
addresses=dns_addresses_v4,
597+
search=None,
598+
)
599+
dns_services_v4 = [dns_service_v4]
600+
services.append(dns_service_v4)
601+
602+
# Note: IPv6 address might be set to IPv4 field
603+
# Not sure if it's a bug
604+
default_route_v6 = None
605+
default_route_v4 = None
606+
if nic.gateway6:
607+
default_route_v6 = network_model.Route(
608+
network_cidr=DEFAULT_GATEWAY_CIDR_IPV6,
609+
gateway=nic.gateway6)
610+
611+
if nic.gateway:
612+
if netaddr.valid_ipv6(nic.gateway):
613+
default_route_v6 = network_model.Route(
614+
network_cidr=DEFAULT_GATEWAY_CIDR_IPV6,
615+
gateway=nic.gateway)
616+
else:
617+
default_route_v4 = network_model.Route(
618+
network_cidr=DEFAULT_GATEWAY_CIDR_IPV4,
619+
gateway=nic.gateway)
620+
621+
routes_v6 = [default_route_v6] if default_route_v6 else []
622+
routes_v4 = [default_route_v4] if default_route_v4 else []
623+
624+
if nic.address6:
625+
net = network_model.Network(
626+
link=link.name,
627+
address_cidr=network_utils.ip_netmask_to_cidr(
628+
nic.address6, nic.netmask6),
629+
routes=routes_v6,
630+
dns_nameservers=dns_services_v6,
631+
)
632+
networks.append(net)
633+
634+
if nic.address:
635+
if netaddr.valid_ipv6(nic.address):
636+
net = network_model.Network(
637+
link=link.name,
638+
address_cidr=network_utils.ip_netmask_to_cidr(
639+
nic.address, nic.netmask),
640+
routes=routes_v6,
641+
dns_nameservers=dns_services_v6,
642+
)
643+
else:
644+
net = network_model.Network(
645+
link=link.name,
646+
address_cidr=network_utils.ip_netmask_to_cidr(
647+
nic.address, nic.netmask),
648+
routes=routes_v4,
649+
dns_nameservers=dns_services_v4,
650+
)
651+
networks.append(net)
652+
653+
return network_model.NetworkDetailsV2(links=links,
654+
networks=networks,
655+
services=services)
656+
550657

551658
class NoCloudConfigDriveService(baseconfigdrive.BaseConfigDriveService):
552659

cloudbaseinit/metadata/services/vmwareguestinfoservice.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from cloudbaseinit.metadata.services import base
2828
from cloudbaseinit.metadata.services import nocloudservice
2929
from cloudbaseinit.osutils import factory as osutils_factory
30+
from cloudbaseinit.utils import network
3031
from cloudbaseinit.utils import serialization
3132

3233
CONF = cloudbaseinit_conf.CONF
@@ -239,3 +240,22 @@ def _process_network_config(self, data):
239240

240241
LOG.debug("network data %s", network)
241242
return {"network": network}
243+
244+
def _get_datasource_instance_meta_data(self):
245+
"""Returns a dictionary with datasource specific instance data
246+
247+
The instance data structure is based on the cloud-init specifications:
248+
https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html
249+
250+
Datasource-specific metadata crawled for the specific cloud platform.
251+
It should closely represent the structure of the cloud metadata
252+
crawled. The structure of content and details provided are entirely
253+
cloud-dependent.
254+
255+
"""
256+
ds = dict()
257+
network_details = self.get_network_details_v2()
258+
host_info = network.get_host_info(self.get_host_name(),
259+
network_details)
260+
ds.update(host_info)
261+
return ds

cloudbaseinit/tests/metadata/services/test_base.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,38 +61,63 @@ def test_get_user_pwd_encryption_key(self, mock_get_public_keys):
6161
result = self._service.get_user_pwd_encryption_key()
6262
self.assertEqual(result, mock_get_public_keys.return_value[0])
6363

64+
@mock.patch('cloudbaseinit.metadata.services.base.'
65+
'BaseMetadataService._get_datasource_instance_meta_data')
6466
@mock.patch('cloudbaseinit.metadata.services.base.'
6567
'BaseMetadataService.get_public_keys')
6668
@mock.patch('cloudbaseinit.metadata.services.base.'
6769
'BaseMetadataService.get_host_name')
6870
@mock.patch('cloudbaseinit.metadata.services.base.'
6971
'BaseMetadataService.get_instance_id')
70-
def test_get_instance_data(self, mock_instance_id, mock_hostname,
71-
mock_public_keys):
72+
def _test_get_instance_data_with_datasource_meta_data(
73+
self, mock_instance_id, mock_hostname, mock_public_keys,
74+
mock_get_datasource_instance_meta_data, datasource_meta_data=None):
7275
fake_instance_id = 'id'
7376
mock_instance_id.return_value = fake_instance_id
7477
fake_hostname = 'host'
7578
mock_hostname.return_value = fake_hostname
7679
fake_keys = ['ssh1', 'ssh2']
7780
mock_public_keys.return_value = fake_keys
81+
mock_get_datasource_instance_meta_data.return_value = \
82+
datasource_meta_data
7883

84+
if datasource_meta_data:
85+
ds_md = datasource_meta_data
86+
else:
87+
ds_md = {
88+
"instance_id": fake_instance_id,
89+
"instance-id": fake_instance_id,
90+
"local_hostname": fake_hostname,
91+
"local-hostname": fake_hostname,
92+
"hostname": fake_hostname
93+
}
7994
expected_response = {
8095
'v1': {
8196
"instance_id": fake_instance_id,
97+
"instance-id": fake_instance_id,
8298
"local_hostname": fake_hostname,
99+
"local-hostname": fake_hostname,
83100
"public_ssh_keys": fake_keys
84101
},
85102
'ds': {
86-
'meta_data': {
87-
"instance_id": fake_instance_id,
88-
"local_hostname": fake_hostname,
89-
"public_ssh_keys": fake_keys,
90-
"hostname": fake_hostname
91-
},
92-
}
103+
'_doc': base.EXPERIMENTAL_NOTICE,
104+
'meta_data': ds_md,
105+
},
106+
"instance_id": fake_instance_id,
107+
"instance-id": fake_instance_id,
108+
"local_hostname": fake_hostname,
109+
"local-hostname": fake_hostname,
110+
"public_ssh_keys": fake_keys,
93111
}
94112
self.assertEqual(expected_response, self._service.get_instance_data())
95113

114+
def test_get_instance_data(self):
115+
self._test_get_instance_data_with_datasource_meta_data()
116+
117+
def test_get_instance_data_with_datasource_meta_data(self):
118+
self._test_get_instance_data_with_datasource_meta_data(
119+
datasource_meta_data={'fake-data': 'fake-value'})
120+
96121

97122
class TestBaseHTTPMetadataService(unittest.TestCase):
98123

0 commit comments

Comments
 (0)