Zhongcheng Lao dc0367425a 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
2024-11-27 12:29:50 +00:00

635 lines
20 KiB
Python

# Copyright 2020 Cloudbase Solutions Srl
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ddt
import importlib
import os
import textwrap
import unittest
import unittest.mock as mock
from cloudbaseinit.metadata.services import base
from cloudbaseinit.metadata.services.nocloudservice import \
NoCloudNetworkConfigParser
from cloudbaseinit.models import network as nm
from cloudbaseinit.tests.metadata import fake_json_response
from cloudbaseinit.tests import testutils
from cloudbaseinit.utils import debiface
from cloudbaseinit.utils import network
from cloudbaseinit.utils import serialization
MODULE_PATH = "cloudbaseinit.metadata.services.nocloudservice"
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_EMPTY_CONFIG = """
network:
version: 1
t: 1
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_CONFIG_IS_NOT_LIST = """
network:
version: 1
config: {
test: abc
}
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_CONFIG_ITEM_IS_NOT_DICT = """
network:
version: 1
config:
- ['test', 'abc']
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_ROUTER_CONFIG_NOT_SUPPORTED = """
network:
version: 1
config:
- type: router
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_LEGACY = """
version: 1
config:
- type: physical
name: interface0
mac_address: "52:54:00:12:34:00"
mtu: 1450
subnets:
- type: static
address: 192.168.1.10
netmask: 255.255.255.0
gateway: 192.168.1.1
dns_nameservers:
- 192.168.1.11
- type: bond
name: bond0
bond_interfaces:
- gbe0
- gbe1
mac_address: "52:54:00:12:34:00"
params:
bond-mode: active-backup
bond-lacp-rate: false
mtu: 1450
subnets:
- type: static
address: 192.168.1.10
netmask: 255.255.255.0
dns_nameservers:
- 192.168.1.11
- type: vlan
name: vlan0
vlan_link: eth1
vlan_id: 150
mac_address: "52:54:00:12:34:00"
mtu: 1450
subnets:
- type: static
address: 192.168.1.10
netmask: 255.255.255.0
dns_nameservers:
- 192.168.1.11
- type: nameserver
address:
- 192.168.23.2
- 8.8.8.8
search: acme.local
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1 = """
network:%s
""" % (textwrap.indent(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_LEGACY, " "))
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_EMPTY_CONFIG = """
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_CONFIG_IS_NOT_DICT = """
network:
- config
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_CONFIG_ITEM_IS_NOT_DICT = """
network:
version: 2
ethernets:
- test
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_CONFIG_ITEM_SETTING_IS_NOT_DICT = """
network:
version: 2
ethernets:
eth0:
- test
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_LEGACY = """
version: 2
ethernets:
interface0:
match:
macaddress: "52:54:00:12:34:00"
set-name: "eth0"
addresses:
- 192.168.1.10/24
gateway4: 192.168.1.1
nameservers:
addresses:
- 192.168.1.11
- 192.168.1.12
search:
- acme.local
mtu: 1450
interface1:
set-name: "interface1"
addresses:
- 192.168.1.100/24
gateway4: 192.168.1.1
nameservers:
addresses:
- 192.168.1.11
- 192.168.1.12
search:
- acme.local
bonds:
bond0:
interfaces: ["gbe0", "gbe1"]
match:
macaddress: "52:54:00:12:34:00"
parameters:
mode: active-backup
lacp-rate: false
addresses:
- 192.168.1.10/24
nameservers:
addresses:
- 192.168.1.11
mtu: 1450
vlans:
vlan0:
id: 150
link: eth1
dhcp4: yes
match:
macaddress: "52:54:00:12:34:00"
addresses:
- 192.168.1.10/24
nameservers:
addresses:
- 192.168.1.11
mtu: 1450
bridges:
br0:
interfaces: ['eth0']
dhcp4: true
"""
NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2 = """
network:%s
""" % (textwrap.indent(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_LEGACY, " "))
@ddt.ddt
class TestNoCloudNetworkConfigV1Parser(unittest.TestCase):
def setUp(self):
module = importlib.import_module(MODULE_PATH)
self._parser = module.NoCloudNetworkConfigV1Parser()
self.snatcher = testutils.LogSnatcher(MODULE_PATH)
@ddt.data(
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_EMPTY_CONFIG,
('Network configuration is empty', None)),
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_CONFIG_IS_NOT_LIST,
("is not a list", None)),
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_CONFIG_ITEM_IS_NOT_DICT,
("is not a dictionary",
nm.NetworkDetailsV2(links=[], networks=[], services=[]))),
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_ROUTER_CONFIG_NOT_SUPPORTED,
("Network config type 'router' is not supported",
nm.NetworkDetailsV2(links=[], networks=[], services=[])))
)
@ddt.unpack
def test_parse_empty_result(self, input, expected_result):
with self.snatcher:
result = self._parser.parse(serialization.parse_json_yaml(input))
self.assertEqual(True, expected_result[0] in self.snatcher.output[0])
self.assertEqual(result, expected_result[1])
@ddt.data(
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1, True),
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V1_LEGACY, True)
)
@ddt.unpack
def test_network_details_v2(self, test_data, expected_result):
expected_bond = nm.Bond(
members=["gbe0", "gbe1"],
type=nm.BOND_TYPE_ACTIVE_BACKUP,
lb_algorithm=None,
lacp_rate=None,
)
expected_link_bond = nm.Link(
id='bond0',
name='bond0',
type=nm.LINK_TYPE_BOND,
enabled=True,
mac_address="52:54:00:12:34:00",
mtu=1450,
bond=expected_bond,
vlan_link=None,
vlan_id=None,
)
expected_link = nm.Link(
id='interface0',
name='interface0',
type=nm.LINK_TYPE_PHYSICAL,
enabled=True,
mac_address="52:54:00:12:34:00",
mtu=1450,
bond=None,
vlan_link=None,
vlan_id=None,
)
expected_link_vlan = nm.Link(
id='vlan0',
name='vlan0',
type=nm.LINK_TYPE_VLAN,
enabled=True,
mac_address="52:54:00:12:34:00",
mtu=1450,
bond=None,
vlan_link='eth1',
vlan_id=150,
)
expected_network = nm.Network(
link='interface0',
address_cidr='192.168.1.10/24',
dns_nameservers=['192.168.1.11'],
routes=[
nm.Route(network_cidr='0.0.0.0/0',
gateway="192.168.1.1")
]
)
expected_network_bond = nm.Network(
link='bond0',
address_cidr='192.168.1.10/24',
dns_nameservers=['192.168.1.11'],
routes=[],
)
expected_network_vlan = nm.Network(
link='vlan0',
address_cidr='192.168.1.10/24',
dns_nameservers=['192.168.1.11'],
routes=[],
)
expected_nameservers = nm.NameServerService(
addresses=['192.168.23.2', '8.8.8.8'],
search='acme.local')
result = self._parser.parse(
serialization.parse_json_yaml(test_data))
self.assertEqual(result.links[0], expected_link)
self.assertEqual(result.networks[0], expected_network)
self.assertEqual(result.links[1], expected_link_bond)
self.assertEqual(result.networks[1], expected_network_bond)
self.assertEqual(result.links[2], expected_link_vlan)
self.assertEqual(result.networks[2], expected_network_vlan)
self.assertEqual(result.services[0], expected_nameservers)
@ddt.ddt
class TestNoCloudNetworkConfigV2Parser(unittest.TestCase):
def setUp(self):
module = importlib.import_module(MODULE_PATH)
self._parser = module.NoCloudNetworkConfigV2Parser()
self.snatcher = testutils.LogSnatcher(MODULE_PATH)
@ddt.data(
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_EMPTY_CONFIG,
('Network configuration is empty', None)),
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_CONFIG_IS_NOT_DICT,
('is not a dict', None)),
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_CONFIG_ITEM_IS_NOT_DICT,
('is not a dict',
nm.NetworkDetailsV2(links=[], networks=[], services=[])),
),
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_CONFIG_ITEM_SETTING_IS_NOT_DICT,
('of type ethernet is not a dict',
nm.NetworkDetailsV2(links=[], networks=[], services=[])),
)
)
@ddt.unpack
def test_parse_empty_result(self, input, expected_result):
with self.snatcher:
result = self._parser.parse(serialization.parse_json_yaml(input))
self.assertEqual(True, expected_result[0] in self.snatcher.output[0])
self.assertEqual(result, expected_result[1])
@ddt.data(
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2, True),
(NOCLOUD_NETWORK_CONFIG_TEST_DATA_V2_LEGACY, True)
)
@ddt.unpack
def test_network_details_v2(self, test_data, expected_result):
expected_bond = nm.Bond(
members=["gbe0", "gbe1"],
type=nm.BOND_TYPE_ACTIVE_BACKUP,
lb_algorithm=None,
lacp_rate=None,
)
expected_link_bond = nm.Link(
id='bond0',
name='bond0',
type=nm.LINK_TYPE_BOND,
enabled=True,
mac_address="52:54:00:12:34:00",
mtu=1450,
bond=expected_bond,
vlan_link=None,
vlan_id=None,
)
expected_link = nm.Link(
id='interface0',
name='eth0',
type=nm.LINK_TYPE_PHYSICAL,
enabled=True,
mac_address="52:54:00:12:34:00",
mtu=1450,
bond=None,
vlan_link=None,
vlan_id=None,
)
expected_link_if1 = nm.Link(
id='interface1',
name='interface1',
type=nm.LINK_TYPE_PHYSICAL,
enabled=True,
mac_address=None,
mtu=None,
bond=None,
vlan_link=None,
vlan_id=None,
)
expected_link_vlan = nm.Link(
id='vlan0',
name='vlan0',
type=nm.LINK_TYPE_VLAN,
enabled=True,
mac_address="52:54:00:12:34:00",
mtu=1450,
bond=None,
vlan_link='eth1',
vlan_id=150,
)
expected_network = nm.Network(
link='eth0',
address_cidr='192.168.1.10/24',
dns_nameservers=['192.168.1.11', '192.168.1.12'],
routes=[
nm.Route(network_cidr='0.0.0.0/0',
gateway="192.168.1.1")
]
)
expected_network_if1 = nm.Network(
link='interface1',
address_cidr='192.168.1.100/24',
dns_nameservers=['192.168.1.11', '192.168.1.12'],
routes=[
nm.Route(network_cidr='0.0.0.0/0',
gateway="192.168.1.1")
]
)
expected_network_bond = nm.Network(
link='bond0',
address_cidr='192.168.1.10/24',
dns_nameservers=['192.168.1.11'],
routes=[],
)
expected_network_vlan = nm.Network(
link='vlan0',
address_cidr='192.168.1.10/24',
dns_nameservers=['192.168.1.11'],
routes=[],
)
expected_nameservers = nm.NameServerService(
addresses=['192.168.1.11', '192.168.1.12'],
search='acme.local')
result = self._parser.parse(
serialization.parse_json_yaml(test_data))
self.assertEqual(result.links[0], expected_link)
self.assertEqual(result.links[1], expected_link_if1)
self.assertEqual(result.networks[0], expected_network)
self.assertEqual(result.networks[1], expected_network_if1)
self.assertEqual(result.links[2], expected_link_bond)
self.assertEqual(result.networks[2], expected_network_bond)
self.assertEqual(result.links[3], expected_link_vlan)
self.assertEqual(result.networks[3], expected_network_vlan)
self.assertEqual(result.services[0], expected_nameservers)
@ddt.ddt
class TestNoCloudConfigDriveService(unittest.TestCase):
def setUp(self):
self._win32com_mock = mock.MagicMock()
self._ctypes_mock = mock.MagicMock()
self._ctypes_util_mock = mock.MagicMock()
self._win32com_client_mock = mock.MagicMock()
self._pywintypes_mock = mock.MagicMock()
self._module_patcher = mock.patch.dict(
'sys.modules',
{'win32com': self._win32com_mock,
'ctypes': self._ctypes_mock,
'ctypes.util': self._ctypes_util_mock,
'win32com.client': self._win32com_client_mock,
'pywintypes': self._pywintypes_mock})
self._module_patcher.start()
self.addCleanup(self._module_patcher.stop)
self.configdrive_module = importlib.import_module(MODULE_PATH)
self._config_drive = (
self.configdrive_module.NoCloudConfigDriveService())
self.snatcher = testutils.LogSnatcher(MODULE_PATH)
@mock.patch('os.path.normpath')
@mock.patch('os.path.join')
def test_get_data(self, mock_join, mock_normpath):
fake_path = os.path.join('fake', 'path')
with mock.patch('builtins.open',
mock.mock_open(read_data='fake data'), create=True):
response = self._config_drive._get_data(fake_path)
self.assertEqual('fake data', response)
mock_join.assert_called_with(
self._config_drive._metadata_path, fake_path)
mock_normpath.assert_called_once_with(mock_join.return_value)
@mock.patch('shutil.rmtree')
def test_cleanup(self, mock_rmtree):
fake_path = os.path.join('fake', 'path')
self._config_drive._metadata_path = fake_path
mock_mgr = mock.Mock()
self._config_drive._mgr = mock_mgr
mock_mgr.target_path = fake_path
self._config_drive.cleanup()
mock_rmtree.assert_called_once_with(fake_path,
ignore_errors=True)
self.assertEqual(None, self._config_drive._metadata_path)
@mock.patch(MODULE_PATH + '.NoCloudConfigDriveService._get_meta_data')
def test_get_public_keys(self, mock_get_metadata):
fake_key = 'fake key'
expected_result = [fake_key]
mock_get_metadata.return_value = {
'public-keys': {
'0': {
'openssh-key': fake_key
}
}
}
result = self._config_drive.get_public_keys()
self.assertEqual(result, expected_result)
@mock.patch(MODULE_PATH + '.NoCloudConfigDriveService._get_meta_data')
def test_get_public_keys_alt_fmt(self, mock_get_metadata):
fake_key = 'fake key'
expected_result = [fake_key]
mock_get_metadata.return_value = {
'public-keys': [fake_key]
}
result = self._config_drive.get_public_keys()
self.assertEqual(result, expected_result)
@ddt.data(('', ('V2 network metadata is empty', None)),
('1', ('V2 network metadata is not a dictionary', None)),
('{}', ('V2 network metadata is empty', None)),
('{}}', ('V2 network metadata could not be deserialized', None)),
(base.NotExistingMetadataException('exc'),
('V2 network metadata not found', True)))
@ddt.unpack
@mock.patch(MODULE_PATH + '.NoCloudConfigDriveService._get_cache_data')
def test_network_details_v2_empty_result(self, input, expected_result,
mock_get_cache_data):
if expected_result[1]:
mock_get_cache_data.side_effect = [input]
else:
mock_get_cache_data.return_value = input
with self.snatcher:
result = self._config_drive.get_network_details_v2()
self.assertEqual(True, expected_result[0] in self.snatcher.output[0])
self.assertEqual(result, None)
mock_get_cache_data.assert_called_with(
"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)