Skip to content

Commit f0fe66b

Browse files
committed
Add network config support to VMware guest info service
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]>
1 parent 04c6424 commit f0fe66b

File tree

5 files changed

+789
-78
lines changed

5 files changed

+789
-78
lines changed

cloudbaseinit/metadata/services/nocloudservice.py

Lines changed: 266 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14+
15+
import copy
1416
import netaddr
1517

1618
from oslo_log import log as oslo_logging
@@ -235,6 +237,10 @@ def parse(self, network_config):
235237
networks = []
236238
services = []
237239

240+
network_config = network_config.get('network') \
241+
if network_config else {}
242+
network_config = network_config.get('config') \
243+
if network_config else None
238244
if not network_config:
239245
LOG.warning("Network configuration is empty")
240246
return
@@ -272,6 +278,265 @@ def parse(self, network_config):
272278
)
273279

274280

281+
class NoCloudNetworkConfigV2Parser(object):
282+
DEFAULT_GATEWAY_CIDR_IPV4 = u"0.0.0.0/0"
283+
DEFAULT_GATEWAY_CIDR_IPV6 = u"::/0"
284+
285+
NETWORK_LINK_TYPE_ETHERNET = 'ethernet'
286+
NETWORK_LINK_TYPE_BOND = 'bond'
287+
NETWORK_LINK_TYPE_VLAN = 'vlan'
288+
NETWORK_LINK_TYPE_BRIDGE = 'bridge'
289+
290+
SUPPORTED_NETWORK_CONFIG_TYPES = {
291+
NETWORK_LINK_TYPE_ETHERNET: 'ethernets',
292+
NETWORK_LINK_TYPE_BOND: 'bonds',
293+
NETWORK_LINK_TYPE_VLAN: 'vlans',
294+
}
295+
296+
def _parse_mac_address(self, item):
297+
return item.get("match", {}).get("macaddress")
298+
299+
def _parse_addresses(self, item, link_name):
300+
networks = []
301+
services = []
302+
303+
routes = []
304+
# handle route config in deprecated gateway4/gateway6
305+
gateway4 = item.get("gateway4")
306+
gateway6 = item.get("gateway6")
307+
default_route = None
308+
if gateway6 and netaddr.valid_ipv6(gateway6):
309+
default_route = network_model.Route(
310+
network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV6,
311+
gateway=gateway6)
312+
elif gateway4 and netaddr.valid_ipv4(gateway4):
313+
default_route = network_model.Route(
314+
network_cidr=self.DEFAULT_GATEWAY_CIDR_IPV4,
315+
gateway=gateway4)
316+
if default_route:
317+
routes.append(default_route)
318+
319+
# netplan format config
320+
routes_config = item.get("routes", {})
321+
for route_config in routes_config:
322+
network_cidr = route_config.get("to")
323+
gateway = route_config.get("via")
324+
if network_cidr.lower() == "default":
325+
if netaddr.valid_ipv6(gateway):
326+
network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV6
327+
else:
328+
network_cidr = self.DEFAULT_GATEWAY_CIDR_IPV4
329+
route = network_model.Route(
330+
network_cidr=network_cidr,
331+
gateway=gateway)
332+
routes.append(route)
333+
334+
nameservers = item.get("nameservers")
335+
nameserver_addresses = nameservers.get("addresses", []) \
336+
if nameservers else []
337+
searches = nameservers.get("search", [])
338+
service = network_model.NameServerService(
339+
addresses=nameserver_addresses,
340+
search=','.join(searches) if searches else None,
341+
)
342+
services.append(service)
343+
344+
addresses = item.get("addresses", [])
345+
for addr in addresses:
346+
network = network_model.Network(
347+
link=link_name,
348+
address_cidr=addr,
349+
dns_nameservers=nameserver_addresses,
350+
routes=routes
351+
)
352+
networks.append(network)
353+
354+
return networks, services
355+
356+
def _parse_ethernet_config_item(self, item):
357+
if not item.get('name'):
358+
LOG.warning("Ethernet does not have a name.")
359+
return
360+
361+
name = item.get('name')
362+
eth_name = item.get("set-name", name)
363+
link = network_model.Link(
364+
id=name,
365+
name=eth_name,
366+
type=network_model.LINK_TYPE_PHYSICAL,
367+
enabled=True,
368+
mac_address=self._parse_mac_address(item),
369+
mtu=item.get('mtu'),
370+
bond=None,
371+
vlan_link=None,
372+
vlan_id=None
373+
)
374+
375+
networks, services = self._parse_addresses(item, link.name)
376+
return network_model.NetworkDetailsV2(
377+
links=[link],
378+
networks=networks,
379+
services=services,
380+
)
381+
382+
def _parse_bond_config_item(self, item):
383+
if not item.get('name'):
384+
LOG.warning("Bond does not have a name.")
385+
return
386+
387+
bond_params = item.get('parameters')
388+
if not bond_params:
389+
LOG.warning("Bond does not have parameters")
390+
return
391+
392+
bond_mode = bond_params.get('mode')
393+
if bond_mode not in network_model.AVAILABLE_BOND_TYPES:
394+
raise exception.CloudbaseInitException(
395+
"Unsupported bond mode: %s" % bond_mode)
396+
397+
bond_lacp_rate = None
398+
if bond_mode == network_model.BOND_TYPE_8023AD:
399+
bond_lacp_rate = bond_params.get('lacp-rate')
400+
if (bond_lacp_rate and bond_lacp_rate not in
401+
network_model.AVAILABLE_BOND_LACP_RATES):
402+
raise exception.CloudbaseInitException(
403+
"Unsupported bond lacp rate: %s" % bond_lacp_rate)
404+
405+
bond_xmit_hash_policy = bond_params.get('transmit-hash-policy')
406+
if (bond_xmit_hash_policy and bond_xmit_hash_policy not in
407+
network_model.AVAILABLE_BOND_LB_ALGORITHMS):
408+
raise exception.CloudbaseInitException(
409+
"Unsupported bond hash policy: %s" %
410+
bond_xmit_hash_policy)
411+
412+
bond_interfaces = item.get('interfaces')
413+
414+
bond = network_model.Bond(
415+
members=bond_interfaces,
416+
type=bond_mode,
417+
lb_algorithm=bond_xmit_hash_policy,
418+
lacp_rate=bond_lacp_rate,
419+
)
420+
421+
link = network_model.Link(
422+
id=item.get('name'),
423+
name=item.get('name'),
424+
type=network_model.LINK_TYPE_BOND,
425+
enabled=True,
426+
mac_address=self._parse_mac_address(item),
427+
mtu=item.get('mtu'),
428+
bond=bond,
429+
vlan_link=None,
430+
vlan_id=None
431+
)
432+
433+
networks, services = self._parse_addresses(item, link.name)
434+
return network_model.NetworkDetailsV2(
435+
links=[link],
436+
networks=networks,
437+
services=services
438+
)
439+
440+
def _parse_vlan_config_item(self, item):
441+
if not item.get('name'):
442+
LOG.warning("VLAN NIC does not have a name.")
443+
return
444+
445+
link = network_model.Link(
446+
id=item.get('name'),
447+
name=item.get('name'),
448+
type=network_model.LINK_TYPE_VLAN,
449+
enabled=True,
450+
mac_address=self._parse_mac_address(item),
451+
mtu=item.get('mtu'),
452+
bond=None,
453+
vlan_link=item.get('link'),
454+
vlan_id=item.get('id')
455+
)
456+
457+
networks, services = self._parse_addresses(item, link.name)
458+
return network_model.NetworkDetailsV2(
459+
links=[link],
460+
networks=networks,
461+
services=services,
462+
)
463+
464+
def _get_network_config_parser(self, parser_type):
465+
parsers = {
466+
self.NETWORK_LINK_TYPE_ETHERNET: self._parse_ethernet_config_item,
467+
self.NETWORK_LINK_TYPE_BOND: self._parse_bond_config_item,
468+
self.NETWORK_LINK_TYPE_VLAN: self._parse_vlan_config_item,
469+
}
470+
parser = parsers.get(parser_type)
471+
if not parser:
472+
raise exception.CloudbaseInitException(
473+
"Network config parser '%s' does not exist",
474+
parser_type)
475+
return parser
476+
477+
def parse(self, network_config):
478+
links = []
479+
networks = []
480+
services = []
481+
482+
network_config = network_config.get('network') \
483+
if network_config else {}
484+
if not network_config:
485+
LOG.warning("Network configuration is empty")
486+
return
487+
488+
if not isinstance(network_config, dict):
489+
LOG.warning("Network config '%s' is not a dict.",
490+
network_config)
491+
return
492+
493+
for singular, plural in self.SUPPORTED_NETWORK_CONFIG_TYPES.items():
494+
network_config_items = network_config.get(plural, {})
495+
if not network_config_items:
496+
continue
497+
498+
if not isinstance(network_config_items, dict):
499+
LOG.warning("Network config '%s' is not a dict",
500+
network_config_items)
501+
continue
502+
503+
for name, network_config_item in network_config_items.items():
504+
if not isinstance(network_config_item, dict):
505+
LOG.warning(
506+
"network config item '%s' of type %s is not a dict",
507+
network_config_item, singular)
508+
continue
509+
510+
item = copy.deepcopy(network_config_item)
511+
item['name'] = name
512+
net_details = (
513+
self._get_network_config_parser(singular)
514+
(item))
515+
516+
if net_details:
517+
links += net_details.links
518+
networks += net_details.networks
519+
services += net_details.services
520+
521+
return network_model.NetworkDetailsV2(
522+
links=links,
523+
networks=networks,
524+
services=services
525+
)
526+
527+
528+
class NoCloudNetworkConfigParser(object):
529+
530+
@staticmethod
531+
def parse(network_data):
532+
network_data_version = network_data.get("network", {}).get("version")
533+
if network_data_version == 1:
534+
network_config_parser = NoCloudNetworkConfigV1Parser()
535+
return network_config_parser.parse(network_data)
536+
537+
return NoCloudNetworkConfigV2Parser().parse(network_data)
538+
539+
275540
class NoCloudConfigDriveService(baseconfigdrive.BaseConfigDriveService):
276541

277542
def __init__(self):
@@ -334,11 +599,4 @@ def get_network_details_v2(self):
334599
LOG.exception("V2 network metadata could not be deserialized")
335600
return
336601

337-
network_data_version = network_data.get("version")
338-
if network_data_version != 1:
339-
LOG.error("Network data version '%s' is not supported",
340-
network_data_version)
341-
return
342-
343-
network_config_parser = NoCloudNetworkConfigV1Parser()
344-
return network_config_parser.parse(network_data.get("config"))
602+
return NoCloudNetworkConfigParser.parse(network_data)

0 commit comments

Comments
 (0)