Write network_data.json metadata to config-dirve
cloud-init will use fall-back config when network metadata is *not* present. This works fine if the first NIC on the node is connected to a network with DHCP. But, when the first NIC is not used for provisioning, cloud-init will only write a fallback config for the first NIC. This causes the provisioned node to be unavailable. Extend instance configuration to include network metadata so that cloud-init can configure node networking. Story: 2009238 Task: 43378 Change-Id: I70f1a972a6d5a0398cd348f00308957386d66067
This commit is contained in:
parent
56cf1fe6f3
commit
f98dfa61c1
93
metalsmith/_network_metadata.py
Normal file
93
metalsmith/_network_metadata.py
Normal file
@ -0,0 +1,93 @@
|
||||
# Copyright 2021 Red Hat, Inc.
|
||||
#
|
||||
# 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 ipaddress
|
||||
|
||||
from openstack import exceptions as sdk_exc
|
||||
|
||||
from metalsmith import exceptions
|
||||
|
||||
|
||||
def create_network_metadata(connection, attached_ports):
|
||||
network_data = {}
|
||||
if not attached_ports:
|
||||
return network_data
|
||||
|
||||
links = network_data.setdefault('links', [])
|
||||
networks = network_data.setdefault('networks', [])
|
||||
services = network_data.setdefault('services', [])
|
||||
|
||||
for attached_port in attached_ports:
|
||||
try:
|
||||
port = connection.network.get_port(attached_port)
|
||||
net = connection.network.get_network(port.network_id)
|
||||
subnets = [connection.network.get_subnet(x['subnet_id'])
|
||||
for x in port.fixed_ips]
|
||||
subnets_by_id = {x.id: x for x in subnets}
|
||||
except sdk_exc.SDKException as exc:
|
||||
raise exceptions.NetworkResourceNotFound(
|
||||
'Cannot find network resource: %s' % exc)
|
||||
|
||||
metadata_add_links(links, port, net)
|
||||
metadata_add_services(services, subnets)
|
||||
for idx, fixed_ip in enumerate(port.fixed_ips):
|
||||
subnet = subnets_by_id[fixed_ip['subnet_id']]
|
||||
metadata_add_network(networks, idx, fixed_ip, port, net, subnet)
|
||||
|
||||
return network_data
|
||||
|
||||
|
||||
def metadata_add_links(links, port, network):
|
||||
links.append({'id': port.id,
|
||||
'type': 'phy',
|
||||
'mtu': network.mtu,
|
||||
'ethernet_mac_address': port.mac_address})
|
||||
|
||||
|
||||
def metadata_add_services(services, subnets):
|
||||
for subnet in subnets:
|
||||
for dns_nameserver in subnet.dns_nameservers:
|
||||
services.append({'type': 'dns',
|
||||
'address': dns_nameserver})
|
||||
|
||||
|
||||
def metadata_add_network(networks, idx, fixed_ip, port, network, subnet):
|
||||
ip_net = ipaddress.ip_network(subnet.cidr)
|
||||
|
||||
net_data = {'id': network.name + str(idx),
|
||||
'network_id': network.id,
|
||||
'link': port.id,
|
||||
'ip_address': fixed_ip['ip_address'],
|
||||
'netmask': str(ip_net.netmask)}
|
||||
|
||||
if subnet.ip_version == 4:
|
||||
net_data['type'] = 'ipv4_dhcp' if subnet.is_dhcp_enabled else 'ipv4'
|
||||
elif subnet.ip_version == 6:
|
||||
net_data['type'] = ('ipv6_{}'.format(subnet.ipv6_address_mode)
|
||||
if subnet.ipv6_address_mode else 'ipv6')
|
||||
|
||||
net_routes = net_data.setdefault('routes', [])
|
||||
for route in subnet.host_routes:
|
||||
ip_net = ipaddress.ip_network(route['destination'])
|
||||
net_routes.append({'network': str(ip_net.network_address),
|
||||
'netmask': str(ip_net.netmask),
|
||||
'gateway': route['nexthop']})
|
||||
|
||||
# Services go in both "network" and toplevel.
|
||||
# Ref: https://docs.openstack.org/nova/latest/_downloads/9119ca7ac90aa2990e762c08baea3a36/network_data.json # noqa
|
||||
net_services = net_data.setdefault('services', [])
|
||||
metadata_add_services(net_services, [subnet])
|
||||
|
||||
networks.append(net_data)
|
@ -19,6 +19,7 @@ from openstack import connection
|
||||
from openstack import exceptions as os_exc
|
||||
|
||||
from metalsmith import _instance
|
||||
from metalsmith import _network_metadata
|
||||
from metalsmith import _nics
|
||||
from metalsmith import _scheduler
|
||||
from metalsmith import _utils
|
||||
@ -427,9 +428,13 @@ class Provisioner(object):
|
||||
node, instance_info=instance_info, extra=extra)
|
||||
self.connection.baremetal.validate_node(node)
|
||||
|
||||
network_data = _network_metadata.create_network_metadata(
|
||||
self.connection, node.extra.get(_ATTACHED_PORTS))
|
||||
|
||||
LOG.debug('Generating a configdrive for node %s',
|
||||
_utils.log_res(node))
|
||||
cd = config.generate(node, _utils.hostname_for(node, allocation))
|
||||
cd = config.generate(node, _utils.hostname_for(node, allocation),
|
||||
network_data)
|
||||
LOG.debug('Starting provisioning of node %s', _utils.log_res(node))
|
||||
self.connection.baremetal.set_node_provision_state(
|
||||
node, 'active', config_drive=cd)
|
||||
|
@ -95,6 +95,10 @@ class InstanceNotFound(Error):
|
||||
"""Instance not found or node doesn't have an instance associated."""
|
||||
|
||||
|
||||
class NetworkResourceNotFound(Error):
|
||||
"""Network resource, port, network, subnet not found"""
|
||||
|
||||
|
||||
# Deprecated aliases
|
||||
DeploymentFailure = DeploymentFailed
|
||||
InvalidInstance = InstanceNotFound
|
||||
|
@ -46,15 +46,18 @@ class GenericConfig(object):
|
||||
'got %r' % meta_data)
|
||||
self.meta_data = meta_data or {}
|
||||
|
||||
def generate(self, node, hostname=None):
|
||||
def generate(self, node, hostname=None, network_data=None):
|
||||
"""Generate the config drive information.
|
||||
|
||||
:param node: `Node` object.
|
||||
:param hostname: Desired hostname (defaults to node's name or ID).
|
||||
:param network_data: Network metadata as dictionary
|
||||
:return: configdrive contents as a dictionary with keys:
|
||||
|
||||
``meta_data``
|
||||
meta data dictionary
|
||||
``network_data``
|
||||
network data as dictionary
|
||||
``user_data``
|
||||
user data as a string
|
||||
"""
|
||||
@ -81,8 +84,12 @@ class GenericConfig(object):
|
||||
|
||||
user_data = self.populate_user_data()
|
||||
|
||||
return {'meta_data': meta_data,
|
||||
'user_data': user_data}
|
||||
data = {'meta_data': meta_data, 'user_data': user_data}
|
||||
|
||||
if network_data:
|
||||
data['network_data'] = network_data
|
||||
|
||||
return data
|
||||
|
||||
def populate_user_data(self):
|
||||
"""Get user data for this configuration.
|
||||
|
@ -29,7 +29,8 @@ class TestGenericConfig(unittest.TestCase):
|
||||
self.node.name = 'node name'
|
||||
|
||||
def _check(self, config, expected_metadata, expected_userdata=None,
|
||||
cloud_init=True, hostname=None):
|
||||
cloud_init=True, hostname=None, network_data=None,
|
||||
expected_network_data=None):
|
||||
expected_m = {'public_keys': {},
|
||||
'uuid': self.node.id,
|
||||
'name': self.node.name,
|
||||
@ -40,7 +41,7 @@ class TestGenericConfig(unittest.TestCase):
|
||||
'meta': {}}
|
||||
expected_m.update(expected_metadata)
|
||||
|
||||
result = config.generate(self.node, hostname)
|
||||
result = config.generate(self.node, hostname, network_data)
|
||||
self.assertEqual(expected_m, result['meta_data'])
|
||||
|
||||
user_data = result['user_data']
|
||||
@ -52,6 +53,11 @@ class TestGenericConfig(unittest.TestCase):
|
||||
user_data = json.loads(user_data)
|
||||
self.assertEqual(expected_userdata, user_data)
|
||||
|
||||
network_data = result.get('network_data')
|
||||
if expected_network_data:
|
||||
self.assertIsNotNone(network_data)
|
||||
self.assertEqual(expected_network_data, network_data)
|
||||
|
||||
def test_default(self):
|
||||
config = self.CLASS()
|
||||
self._check(config, {})
|
||||
@ -85,6 +91,11 @@ class TestGenericConfig(unittest.TestCase):
|
||||
def test_custom_metadata_not_dict(self):
|
||||
self.assertRaises(TypeError, self.CLASS, meta_data="foobar")
|
||||
|
||||
def test_custom_network_data(self):
|
||||
config = self.CLASS()
|
||||
data = {'net': 'data'}
|
||||
self._check(config, {}, network_data=data, expected_network_data=data)
|
||||
|
||||
|
||||
class TestCloudInitConfig(TestGenericConfig):
|
||||
CLASS = instance_config.CloudInitConfig
|
||||
|
130
metalsmith/test/test_network_metadata.py
Normal file
130
metalsmith/test/test_network_metadata.py
Normal file
@ -0,0 +1,130 @@
|
||||
# Copyright 2021 Red Hat, Inc.
|
||||
#
|
||||
# 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 unittest
|
||||
from unittest import mock
|
||||
|
||||
from metalsmith import _network_metadata
|
||||
|
||||
|
||||
class TestMetadataAdd(unittest.TestCase):
|
||||
|
||||
def test_metadata_add_links(self):
|
||||
port = mock.Mock()
|
||||
network = mock.Mock()
|
||||
port.id = 'port_id'
|
||||
port.mac_address = 'aa:bb:cc:dd:ee:ff'
|
||||
network.mtu = 1500
|
||||
links = []
|
||||
expected = [{'id': 'port_id',
|
||||
'type': 'phy',
|
||||
'mtu': 1500,
|
||||
'ethernet_mac_address': 'aa:bb:cc:dd:ee:ff'}]
|
||||
_network_metadata.metadata_add_links(links, port, network)
|
||||
self.assertEqual(expected, links)
|
||||
|
||||
def test_metadata_add_services(self):
|
||||
subnet_a = mock.Mock()
|
||||
subnet_b = mock.Mock()
|
||||
subnet_a.dns_nameservers = ['192.0.2.1', '192.0.2.2']
|
||||
subnet_b.dns_nameservers = ['192.0.2.11', '192.0.2.22']
|
||||
subnets = [subnet_a, subnet_b]
|
||||
services = []
|
||||
expected = [{'address': '192.0.2.1', 'type': 'dns'},
|
||||
{'address': '192.0.2.2', 'type': 'dns'},
|
||||
{'address': '192.0.2.11', 'type': 'dns'},
|
||||
{'address': '192.0.2.22', 'type': 'dns'}]
|
||||
_network_metadata.metadata_add_services(services, subnets)
|
||||
self.assertEqual(expected, services)
|
||||
|
||||
def test_metadata_add_network_ipv4_dhcp(self):
|
||||
idx = 1
|
||||
fixed_ip = {'ip_address': '192.0.2.100', 'subnet_id': 'subnet_id'}
|
||||
|
||||
port = mock.Mock()
|
||||
port.id = 'port_id'
|
||||
|
||||
subnet = mock.Mock()
|
||||
subnet.cidr = '192.0.2.0/26'
|
||||
subnet.ip_version = 4
|
||||
subnet.is_dhcp_enabled = True
|
||||
subnet.host_routes = [
|
||||
{'destination': '192.0.2.64/26', 'nexthop': '192.0.2.1'},
|
||||
{'destination': '192.0.2.128/26', 'nexthop': '192.0.2.1'}
|
||||
]
|
||||
subnet.dns_nameservers = ['192.0.2.11', '192.0.2.22']
|
||||
|
||||
network = mock.Mock()
|
||||
network.id = 'network_id'
|
||||
network.name = 'net_name'
|
||||
|
||||
networks = []
|
||||
expected = [{'id': 'net_name1',
|
||||
'ip_address': '192.0.2.100',
|
||||
'link': 'port_id',
|
||||
'netmask': '255.255.255.192',
|
||||
'network_id': 'network_id',
|
||||
'routes': [{'gateway': '192.0.2.1',
|
||||
'netmask': '255.255.255.192',
|
||||
'network': '192.0.2.64'},
|
||||
{'gateway': '192.0.2.1',
|
||||
'netmask': '255.255.255.192',
|
||||
'network': '192.0.2.128'}],
|
||||
'services': [{'address': '192.0.2.11', 'type': 'dns'},
|
||||
{'address': '192.0.2.22', 'type': 'dns'}],
|
||||
'type': 'ipv4_dhcp'}]
|
||||
_network_metadata.metadata_add_network(networks, idx, fixed_ip, port,
|
||||
network, subnet)
|
||||
self.assertEqual(expected, networks)
|
||||
|
||||
def test_metadata_add_network_ipv6_stateful(self):
|
||||
idx = 1
|
||||
fixed_ip = {'ip_address': '2001:db8:1::10', 'subnet_id': 'subnet_id'}
|
||||
port = mock.Mock()
|
||||
port.id = 'port_id'
|
||||
|
||||
subnet = mock.Mock()
|
||||
subnet.cidr = '2001:db8:1::/64'
|
||||
subnet.ip_version = 6
|
||||
subnet.ipv6_address_mode = 'dhcpv6-stateful'
|
||||
subnet.host_routes = [
|
||||
{'destination': '2001:db8:2::/64', 'nexthop': '2001:db8:1::1'},
|
||||
{'destination': '2001:db8:3::/64', 'nexthop': '2001:db8:1::1'}
|
||||
]
|
||||
subnet.dns_nameservers = ['2001:db8:1::ee', '2001:db8:2::ff']
|
||||
|
||||
network = mock.Mock()
|
||||
network.id = 'network_id'
|
||||
network.name = 'net_name'
|
||||
|
||||
networks = []
|
||||
expected = [
|
||||
{'id': 'net_name1',
|
||||
'ip_address': '2001:db8:1::10',
|
||||
'link': 'port_id',
|
||||
'netmask': 'ffff:ffff:ffff:ffff::',
|
||||
'network_id': 'network_id',
|
||||
'routes': [{'gateway': '2001:db8:1::1',
|
||||
'netmask': 'ffff:ffff:ffff:ffff::',
|
||||
'network': '2001:db8:2::'},
|
||||
{'gateway': '2001:db8:1::1',
|
||||
'netmask': 'ffff:ffff:ffff:ffff::',
|
||||
'network': '2001:db8:3::'}],
|
||||
'services': [{'address': '2001:db8:1::ee', 'type': 'dns'},
|
||||
{'address': '2001:db8:2::ff', 'type': 'dns'}],
|
||||
'type': 'ipv6_dhcpv6-stateful'}]
|
||||
_network_metadata.metadata_add_network(networks, idx, fixed_ip, port,
|
||||
network, subnet)
|
||||
self.assertEqual(expected, networks)
|
@ -20,6 +20,7 @@ from openstack import exceptions as os_exc
|
||||
import requests
|
||||
|
||||
from metalsmith import _instance
|
||||
from metalsmith import _network_metadata
|
||||
from metalsmith import _provisioner
|
||||
from metalsmith import _utils
|
||||
from metalsmith import exceptions
|
||||
@ -510,6 +511,12 @@ class TestProvisionNode(Base):
|
||||
self.configdrive_mock = configdrive_patcher.start()
|
||||
self.addCleanup(configdrive_patcher.stop)
|
||||
|
||||
create_network_metadata_patches = mock.patch.object(
|
||||
_network_metadata, 'create_network_metadata', autospec=True
|
||||
)
|
||||
self.network_metadata_mock = create_network_metadata_patches.start()
|
||||
self.addCleanup(create_network_metadata_patches.stop)
|
||||
|
||||
self.api.baremetal.get_node.side_effect = lambda _n: self.node
|
||||
self.api.baremetal.get_allocation.side_effect = (
|
||||
lambda _a: self.allocation)
|
||||
@ -531,7 +538,8 @@ class TestProvisionNode(Base):
|
||||
self.node, instance_info=self.instance_info, extra=self.extra)
|
||||
self.api.baremetal.validate_node.assert_called_once_with(self.node)
|
||||
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
|
||||
self.allocation.name)
|
||||
self.allocation.name,
|
||||
mock.ANY)
|
||||
self.api.baremetal.set_node_provision_state.assert_called_once_with(
|
||||
self.node, 'active', config_drive=mock.ANY)
|
||||
self.assertFalse(self.api.network.delete_port.called)
|
||||
@ -553,7 +561,7 @@ class TestProvisionNode(Base):
|
||||
self.node, instance_info=self.instance_info, extra=self.extra)
|
||||
self.api.baremetal.validate_node.assert_called_once_with(self.node)
|
||||
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
|
||||
self.node.name)
|
||||
self.node.name, mock.ANY)
|
||||
self.api.baremetal.set_node_provision_state.assert_called_once_with(
|
||||
self.node, 'active', config_drive=mock.ANY)
|
||||
self.assertFalse(self.api.network.delete_port.called)
|
||||
@ -610,7 +618,7 @@ class TestProvisionNode(Base):
|
||||
self.assertEqual(inst.node, self.node)
|
||||
|
||||
config.generate.assert_called_once_with(self.node,
|
||||
self.allocation.name)
|
||||
self.allocation.name, mock.ANY)
|
||||
self.api.network.create_port.assert_called_once_with(
|
||||
network_id=self.api.network.find_network.return_value.id,
|
||||
name='example.com-%s' %
|
||||
@ -660,7 +668,7 @@ class TestProvisionNode(Base):
|
||||
self.node, instance_info=self.instance_info, extra=self.extra)
|
||||
self.api.baremetal.validate_node.assert_called_once_with(self.node)
|
||||
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
|
||||
hostname)
|
||||
hostname, mock.ANY)
|
||||
self.api.baremetal.set_node_provision_state.assert_called_once_with(
|
||||
self.node, 'active', config_drive=mock.ANY)
|
||||
self.assertFalse(
|
||||
@ -688,7 +696,7 @@ class TestProvisionNode(Base):
|
||||
self.node, instance_info=self.instance_info, extra=self.extra)
|
||||
self.api.baremetal.validate_node.assert_called_once_with(self.node)
|
||||
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
|
||||
hostname)
|
||||
hostname, mock.ANY)
|
||||
self.api.baremetal.set_node_provision_state.assert_called_once_with(
|
||||
self.node, 'active', config_drive=mock.ANY)
|
||||
self.assertFalse(
|
||||
@ -718,7 +726,7 @@ class TestProvisionNode(Base):
|
||||
self.node, instance_info=self.instance_info, extra=self.extra)
|
||||
self.api.baremetal.validate_node.assert_called_once_with(self.node)
|
||||
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
|
||||
hostname)
|
||||
hostname, mock.ANY)
|
||||
self.api.baremetal.set_node_provision_state.assert_called_once_with(
|
||||
self.node, 'active', config_drive=mock.ANY)
|
||||
self.assertFalse(
|
||||
@ -775,7 +783,7 @@ class TestProvisionNode(Base):
|
||||
self.node, extra=self.extra, instance_info=self.instance_info)
|
||||
self.api.baremetal.validate_node.assert_called_once_with(self.node)
|
||||
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
|
||||
self.node.name)
|
||||
self.node.name, mock.ANY)
|
||||
self.api.baremetal.set_node_provision_state.assert_called_once_with(
|
||||
self.node, 'active', config_drive=mock.ANY)
|
||||
self.assertFalse(
|
||||
@ -808,7 +816,7 @@ class TestProvisionNode(Base):
|
||||
self.node, extra=self.extra, instance_info=self.instance_info)
|
||||
self.api.baremetal.validate_node.assert_called_once_with(self.node)
|
||||
self.configdrive_mock.assert_called_once_with(mock.ANY, self.node,
|
||||
self.node.id)
|
||||
self.node.id, mock.ANY)
|
||||
self.api.baremetal.set_node_provision_state.assert_called_once_with(
|
||||
self.node, 'active', config_drive=mock.ANY)
|
||||
self.assertFalse(
|
||||
|
13
releasenotes/notes/network-metadata-ff0c3e80e5e0f53c.yaml
Normal file
13
releasenotes/notes/network-metadata-ff0c3e80e5e0f53c.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Network metadata is now created and written to the instance config in the
|
||||
config-drive for deployed nodes.
|
||||
fixes:
|
||||
- |
|
||||
Fixed and issue where deployed nodes did not become available over the
|
||||
network. This happened when the first network interface was not connected
|
||||
to a network with a DHCP service, i.e a secondary network interface was
|
||||
used. The addition of network metadata in the instance config solves this
|
||||
problem. See bug:
|
||||
`2009238 <https://storyboard.openstack.org/#!/story/2009238>`_.
|
Loading…
Reference in New Issue
Block a user