Skip to content

Commit

Permalink
Add network config support to VMware guest info service
Browse files Browse the repository at this point in the history
This commit adds the network config support for VMware environment.
Both NetworkConfig V1 and V2 formats are supported to align with
cloud-init.

NetworkConfig V2 format specification can be found at
https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v2.html

Change-Id: I6c686a88be253858e9f46599f10dca571a76b85e
Signed-off-by: Zhongcheng Lao <[email protected]>
  • Loading branch information
laozc committed May 17, 2024
1 parent 04c6424 commit f0fe66b
Show file tree
Hide file tree
Showing 5 changed files with 789 additions and 78 deletions.
274 changes: 266 additions & 8 deletions cloudbaseinit/metadata/services/nocloudservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# 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 copy
import netaddr

from oslo_log import log as oslo_logging
Expand Down Expand Up @@ -235,6 +237,10 @@ def parse(self, network_config):
networks = []
services = []

network_config = network_config.get('network') \
if network_config else {}
network_config = network_config.get('config') \
if network_config else None
if not network_config:
LOG.warning("Network configuration is empty")
return
Expand Down Expand Up @@ -272,6 +278,265 @@ def parse(self, network_config):
)


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_BOND = 'bond'
NETWORK_LINK_TYPE_VLAN = 'vlan'
NETWORK_LINK_TYPE_BRIDGE = 'bridge'

SUPPORTED_NETWORK_CONFIG_TYPES = {
NETWORK_LINK_TYPE_ETHERNET: 'ethernets',
NETWORK_LINK_TYPE_BOND: 'bonds',
NETWORK_LINK_TYPE_VLAN: 'vlans',
}

def _parse_mac_address(self, item):
return item.get("match", {}).get("macaddress")

def _parse_addresses(self, item, link_name):
networks = []
services = []

routes = []
# handle route config in deprecated gateway4/gateway6
gateway4 = item.get("gateway4")
gateway6 = item.get("gateway6")
default_route = None
if gateway6 and netaddr.valid_ipv6(gateway6):
default_route = network_model.Route(
network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV6,
gateway=gateway6)
elif gateway4 and netaddr.valid_ipv4(gateway4):
default_route = network_model.Route(
network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV4,
gateway=gateway4)
if default_route:
routes.append(default_route)

# netplan format config
routes_config = item.get("routes", {})
for route_config in routes_config:
network_cidr = route_config.get("to")
gateway = route_config.get("via")
if network_cidr.lower() == "default":
if netaddr.valid_ipv6(gateway):
network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV6
else:
network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV4
route = network_model.Route(
network_cidr=network_cidr,
gateway=gateway)
routes.append(route)

nameservers = item.get("nameservers")
nameserver_addresses = nameservers.get("addresses", []) \
if nameservers else []
searches = nameservers.get("search", [])
service = network_model.NameServerService(
addresses=nameserver_addresses,
search=','.join(searches) if searches else None,
)
services.append(service)

addresses = item.get("addresses", [])
for addr in addresses:
network = network_model.Network(
link=link_name,
address_cidr=addr,
dns_nameservers=nameserver_addresses,
routes=routes
)
networks.append(network)

return networks, services

def _parse_ethernet_config_item(self, item):
if not item.get('name'):
LOG.warning("Ethernet does not have a name.")
return

name = item.get('name')
eth_name = item.get("set-name", name)
link = network_model.Link(
id=name,
name=eth_name,
type=network_model.LINK_TYPE_PHYSICAL,
enabled=True,
mac_address=self._parse_mac_address(item),
mtu=item.get('mtu'),
bond=None,
vlan_link=None,
vlan_id=None
)

networks, services = self._parse_addresses(item, link.name)
return network_model.NetworkDetailsV2(
links=[link],
networks=networks,
services=services,
)

def _parse_bond_config_item(self, item):
if not item.get('name'):
LOG.warning("Bond does not have a name.")
return

bond_params = item.get('parameters')
if not bond_params:
LOG.warning("Bond does not have parameters")
return

bond_mode = bond_params.get('mode')
if bond_mode not in network_model.AVAILABLE_BOND_TYPES:
raise exception.CloudbaseInitException(
"Unsupported bond mode: %s" % bond_mode)

bond_lacp_rate = None
if bond_mode == network_model.BOND_TYPE_8023AD:
bond_lacp_rate = bond_params.get('lacp-rate')
if (bond_lacp_rate and bond_lacp_rate not in
network_model.AVAILABLE_BOND_LACP_RATES):
raise exception.CloudbaseInitException(
"Unsupported bond lacp rate: %s" % bond_lacp_rate)

bond_xmit_hash_policy = bond_params.get('transmit-hash-policy')
if (bond_xmit_hash_policy and bond_xmit_hash_policy not in
network_model.AVAILABLE_BOND_LB_ALGORITHMS):
raise exception.CloudbaseInitException(
"Unsupported bond hash policy: %s" %
bond_xmit_hash_policy)

bond_interfaces = item.get('interfaces')

bond = network_model.Bond(
members=bond_interfaces,
type=bond_mode,
lb_algorithm=bond_xmit_hash_policy,
lacp_rate=bond_lacp_rate,
)

link = network_model.Link(
id=item.get('name'),
name=item.get('name'),
type=network_model.LINK_TYPE_BOND,
enabled=True,
mac_address=self._parse_mac_address(item),
mtu=item.get('mtu'),
bond=bond,
vlan_link=None,
vlan_id=None
)

networks, services = self._parse_addresses(item, link.name)
return network_model.NetworkDetailsV2(
links=[link],
networks=networks,
services=services
)

def _parse_vlan_config_item(self, item):
if not item.get('name'):
LOG.warning("VLAN NIC does not have a name.")
return

link = network_model.Link(
id=item.get('name'),
name=item.get('name'),
type=network_model.LINK_TYPE_VLAN,
enabled=True,
mac_address=self._parse_mac_address(item),
mtu=item.get('mtu'),
bond=None,
vlan_link=item.get('link'),
vlan_id=item.get('id')
)

networks, services = self._parse_addresses(item, link.name)
return network_model.NetworkDetailsV2(
links=[link],
networks=networks,
services=services,
)

def _get_network_config_parser(self, parser_type):
parsers = {
self.NETWORK_LINK_TYPE_ETHERNET: self._parse_ethernet_config_item,
self.NETWORK_LINK_TYPE_BOND: self._parse_bond_config_item,
self.NETWORK_LINK_TYPE_VLAN: self._parse_vlan_config_item,
}
parser = parsers.get(parser_type)
if not parser:
raise exception.CloudbaseInitException(
"Network config parser '%s' does not exist",
parser_type)
return parser

def parse(self, network_config):
links = []
networks = []
services = []

network_config = network_config.get('network') \
if network_config else {}
if not network_config:
LOG.warning("Network configuration is empty")
return

if not isinstance(network_config, dict):
LOG.warning("Network config '%s' is not a dict.",
network_config)
return

for singular, plural in self.SUPPORTED_NETWORK_CONFIG_TYPES.items():
network_config_items = network_config.get(plural, {})
if not network_config_items:
continue

if not isinstance(network_config_items, dict):
LOG.warning("Network config '%s' is not a dict",
network_config_items)
continue

for name, network_config_item in network_config_items.items():
if not isinstance(network_config_item, dict):
LOG.warning(
"network config item '%s' of type %s is not a dict",
network_config_item, singular)
continue

item = copy.deepcopy(network_config_item)
item['name'] = name
net_details = (
self._get_network_config_parser(singular)
(item))

if net_details:
links += net_details.links
networks += net_details.networks
services += net_details.services

return network_model.NetworkDetailsV2(
links=links,
networks=networks,
services=services
)


class NoCloudNetworkConfigParser(object):

@staticmethod
def parse(network_data):
network_data_version = network_data.get("network", {}).get("version")
if network_data_version == 1:
network_config_parser = NoCloudNetworkConfigV1Parser()
return network_config_parser.parse(network_data)

return NoCloudNetworkConfigV2Parser().parse(network_data)


class NoCloudConfigDriveService(baseconfigdrive.BaseConfigDriveService):

def __init__(self):
Expand Down Expand Up @@ -334,11 +599,4 @@ def get_network_details_v2(self):
LOG.exception("V2 network metadata could not be deserialized")
return

network_data_version = network_data.get("version")
if network_data_version != 1:
LOG.error("Network data version '%s' is not supported",
network_data_version)
return

network_config_parser = NoCloudNetworkConfigV1Parser()
return network_config_parser.parse(network_data.get("config"))
return NoCloudNetworkConfigParser.parse(network_data)
Loading

0 comments on commit f0fe66b

Please sign in to comment.