Browse Source

tool: convert heat network-config to ansible j2

Add a script that does a best effort conversion of
a heat network-config tempalte to an ansible j2
network-config template.

The script uses a networks data file as input to
genereta a map of Heat Parameters to ansible vars.
For parameters not in the generated map the stack
environment is used, parameter values from the
stack environment is hard-coded in the j2 template.

A j2 comment is added whenever a value was hard coded,
in a header comment and also an inline comment if
possible.

NOTE: The j2 reference files in the unit tests was
      created by converting heat templates.

Change-Id: I8165a077b87307ca3c2ebee54703a939517dc9bf
changes/66/767466/5
Harald Jensås 8 months ago
parent
commit
7de39925d0
  1. 0
      tools/__init__.py
  2. 512
      tools/convert_heat_nic_config_to_ansible_j2.py
  3. 0
      tools/tests/__init__.py
  4. 344
      tools/tests/nic_config_convert_samples/heat_templates/2-linux-bonds-vlans-controller.yaml
  5. 298
      tools/tests/nic_config_convert_samples/heat_templates/bond-vlans-controller.yaml
  6. 239
      tools/tests/nic_config_convert_samples/heat_templates/complex.yaml
  7. 280
      tools/tests/nic_config_convert_samples/heat_templates/multiple-nics-vlans-controller.yaml
  8. 51
      tools/tests/nic_config_convert_samples/heat_templates/simple.yaml
  9. 285
      tools/tests/nic_config_convert_samples/heat_templates/single-nic-linux-bridge-vlans-controller.yaml
  10. 281
      tools/tests/nic_config_convert_samples/heat_templates/single-nic-vlans-controller.yaml
  11. 95
      tools/tests/nic_config_convert_samples/j2_references/2-linux-bonds-vlans-controller.j2
  12. 61
      tools/tests/nic_config_convert_samples/j2_references/bond-vlans-controller.j2
  13. 48
      tools/tests/nic_config_convert_samples/j2_references/complex_complete.j2
  14. 39
      tools/tests/nic_config_convert_samples/j2_references/complex_incomplete.j2
  15. 78
      tools/tests/nic_config_convert_samples/j2_references/multiple-nics-vlans-controller.j2
  16. 26
      tools/tests/nic_config_convert_samples/j2_references/simple.j2
  17. 56
      tools/tests/nic_config_convert_samples/j2_references/single-nic-linux-bridge-vlans-controller.j2
  18. 51
      tools/tests/nic_config_convert_samples/j2_references/single-nic-vlans-controller.j2
  19. 32
      tools/tests/nic_config_convert_samples/network_file_complex.yaml
  20. 4
      tools/tests/nic_config_convert_samples/networks_file_simple.yaml
  21. 17
      tools/tests/nic_config_convert_samples/stack_env_complex.yaml
  22. 10
      tools/tests/nic_config_convert_samples/stack_env_simple.yaml
  23. 303
      tools/tests/test_convert_heat_nic_config_to_ansible_j2.py
  24. 3
      tools/yaml-validate.py

0
tools/__init__.py

512
tools/convert_heat_nic_config_to_ansible_j2.py

@ -0,0 +1,512 @@
#!/usr/bin/env python3
# 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 argparse
import collections
import openstack
import os
import re
import sys
import yaml
MIN_VIABLE_MTU_HEADER = (
"{% set mtu_list = [ctlplane_mtu] %}\n"
"{% for network in role_networks %}\n"
"{{ mtu_list.append(lookup('vars', networks_lower[network] ~ '_mtu')) }}\n"
"{%- endfor %}\n"
"{% set min_viable_mtu = mtu_list | max %}\n"
)
DUAL_MIN_VIABLE_MTU_HEADER = (
"{% set mtu_ctlplane_list = [ctlplane_mtu] %}\n"
"{% set mtu_dataplane_list = [] %}\n"
"{% for network in role_networks %}\n"
"{# This block resolves the minimum viable MTU for interfaces connected to #}\n" # noqa
"{# the dataplane network(s), which start by Tenant, and also bonds #}\n"
"{# and bridges that carry multiple VLANs. Each VLAN may have different MTU. #}\n" # noqa
"{# The bridge, bond or interface must have an MTU to allow the VLAN with the #}\n" # noqa
"{# largest MTU. #}\n"
"{% if network.startswith('Tenant') %}\n"
"{{ mtu_dataplane_list.append(lookup('vars', networks_lower[network] ~ '_mtu')) }}\n" # noqa
"{# This block resolves the minimum viable MTU for interfaces connected to #}\n" # noqa
"{# the control plane network(s) (don't start by Tenant), and also bonds #}\n" # noqa
"{# and bridges that carry multiple VLANs. Each VLAN may have different MTU. #}\n" # noqa
"{# The bridge, bond or interface must have an MTU to allow the VLAN with the #}\n" # noqa
"{# largest MTU. #}\n"
"{% else %}\n"
"{{ mtu_ctlplane_list.append(lookup('vars', networks_lower[network] ~ '_mtu')) }}\n" # noqa
"{%- endfor %}\n"
"{% set min_viable_mtu_ctlplane = mtu_ctlplane_list | max %}\n"
"{% set min_viable_mtu_dataplane = mtu_dataplane_list | max %}\n"
)
UNSUPPORTED_HEAT_INTRINSIC_FUNCTIONS = {
'get_file', 'get_resource', 'digest', 'repeat', 'resource_facade',
'str_replace', 'str_replace_strict', 'str_split', 'map_merge',
'map_replace', 'yaql', 'equals', 'if', 'not', 'and', 'or', 'filter',
'make_url', 'contains'
}
QUOTE_FIX = '%_fix_quote_%'
DBL_QUOTE_FIX = '%_double_fix_quote_%'
def parse_opts(argv):
parser = argparse.ArgumentParser(
description='Convert to Ansible Jinja2 NIC config templates.')
parser.add_argument('--stack',
metavar='STACK_NAME',
help='Name or ID of heat stack (default=overcloud)',
default='overcloud')
parser.add_argument('-n', '--networks_file',
metavar='<network_data.yaml>',
required=True,
help=('Configuration file describing the network '
'deployment.'))
parser.add_argument('-y', '--yes',
default=False,
action='store_true',
help='Overwrite existing files.')
parser.add_argument('template',
metavar='TEMPLATE_FILE',
help='Existing NIC config template to convert.')
opts = parser.parse_args(argv[1:])
return opts
class TemplateDumper(yaml.SafeDumper):
def represent_ordered_dict(self, data):
return self.represent_dict(data.items())
class TemplateLoader(yaml.SafeLoader):
def construct_mapping(self, node):
self.flatten_mapping(node)
return collections.OrderedDict(self.construct_pairs(node))
TemplateDumper.add_representer(collections.OrderedDict,
TemplateDumper.represent_ordered_dict)
TemplateLoader.add_constructor(yaml.BaseLoader,
TemplateLoader.construct_mapping)
class ConvertToAnsibleJ2(object):
def __init__(self, stack_env, networks_file):
self.stack_env = stack_env
self.param_to_var_map = self.create_param_to_var_map(networks_file)
self.hard_coded_parameters = list()
@staticmethod
def unwrap_j2_var(x):
"""Strip jinja2 brackets from string
When nesting ansible vars in jinja2 the brackets must be
removed. This also adds appropriate quote fix prefix and
suffix.
"""
is_ansible_var = False
if isinstance(x, str):
if x.startswith('{{ ') and x.endswith(' }}'):
is_ansible_var = True
x = x[3:]
x = x[:-3]
else:
raise RuntimeError(
'Unsupported type {} for method unwrap_j2_var'.format(type(x)))
if is_ansible_var:
return DBL_QUOTE_FIX + '{}'.format(x) + DBL_QUOTE_FIX
else:
return QUOTE_FIX + '{}'.format(x) + QUOTE_FIX
@staticmethod
def strip_j2_comment(x):
"""Strip jinja2 comment from string
When nesting hard-coded parameter conversions in jinja2 the
comment must be removed.
"""
return re.sub('{#.*#}', '', x)
def normalize_complex(self, old):
if isinstance(old, list):
new = list()
for i in old:
if isinstance(i, str):
new.append(self.strip_j2_comment(self.unwrap_j2_var(i)))
if isinstance(i, (bool, int)):
new.append(i)
if isinstance(i, (list, dict)):
new.append(self.normalize_complex(i))
elif isinstance(old, dict):
new = dict()
for k, v in old.items():
k = QUOTE_FIX + '{}'.format(k) + QUOTE_FIX
if isinstance(v, str):
new[k] = self.strip_j2_comment(self.unwrap_j2_var(v))
if isinstance(v, (bool, int)):
new[k] = v
if isinstance(v, (list, dict)):
new[k] = self.normalize_complex(v)
else:
raise RuntimeError(
'Unsupported type {} for method normalize_complex'.format(
type(old)))
return new
@staticmethod
def to_j2_var(x):
if not isinstance(x, str):
raise RuntimeError(
'Unsupported type {} for method to_j2_var'.format(type(x)))
return '{{{{ {} }}}}'.format(x)
@staticmethod
def quote_fix(x):
return QUOTE_FIX + '{}'.format(x) + QUOTE_FIX
def create_param_to_var_map(self, networks_file):
_map = {
'ControlPlaneIp': self.to_j2_var('ctlplane_ip'),
'ControlPlaneSubnetCidr': self.to_j2_var('ctlplane_subnet_cidr'),
'ControlPlaneMtu': self.to_j2_var('ctlplane_mtu'),
'ControlPlaneDefaultRoute': self.to_j2_var('ctlplane_gateway_ip'),
'ControlPlaneStaticRoutes': self.to_j2_var('ctlplane_host_routes'),
'DnsServers': self.to_j2_var('ctlplane_dns_nameservers'),
'DnsSearchDomains': self.to_j2_var('dns_search_domains'),
'NumDpdkInterfaceRxQueues':
self.to_j2_var('num_dpdk_interface_rx_queues'),
'BondInterfaceOvsOptions':
self.to_j2_var('bond_interface_ovs_options')
}
with open(networks_file, 'r') as f:
networks = yaml.safe_load(f.read())
for net in networks:
name = net['name']
name_lower = net.get('name_lower', net['name'].lower())
_map.update({
name + 'NetworkVlanID':
self.to_j2_var('{}_vlan_id'.format(name_lower)),
name + 'IpSubnet':
'/'.join([self.to_j2_var('{}_ip'.format(name_lower)),
self.to_j2_var('{}_cidr'.format(name_lower))]),
name + 'InterfaceDefaultRoute':
self.to_j2_var('{}_gateway_ip'.format(name_lower)),
name + 'InterfaceRoutes':
self.to_j2_var('{}_host_routes'.format(name_lower)),
name + 'Mtu':
self.to_j2_var('{}_mtu'.format(name_lower))
})
return _map
def convert_get_param(self, old):
param = old['get_param']
if isinstance(param, str):
if param in self.param_to_var_map:
return self.param_to_var_map[param]
elif param in self.stack_env.get('parameter_defaults', {}):
stack_value = self.stack_env['parameter_defaults'][param]
print('INFO - Custom Parameter {} was hard-coded in the '
'converted template using the value from the Heat stack '
'environment.\n'
' To parameterize the value an ansible var must be '
'added using the {{role.name}}ExtraGroupVars '
'THT interface and the template modified to use the '
'ansible var.'.format(param))
j2_comment = (
'{{# NOTE! Custom parameter {} was hard-coded in '
'the converted template. To parameterize use the '
'{{role.name}}ExtraGroupVars THT interface and update the '
'template to use an ansible var. #}}'.format(param))
self.hard_coded_parameters.append(param)
if isinstance(stack_value, str):
return self.quote_fix(stack_value + j2_comment)
else:
return stack_value
else:
print('WARNING - Manual intervention required. Unable to '
'convert get_param occurrence: {}'.format(old))
return self.quote_fix(
'NEED MANUAL CONVERSION: {}'.format(str(old)))
elif isinstance(param, list):
print('WARNING - can not convert get_param referencing values in '
'complex datastructures. Please review the Ansible Jinja2 '
'template and convert this manually.')
return self.quote_fix(
'NEED MANUAL CONVERSION: {}'.format(str(old)))
raise RuntimeError(
'Unexpected Type {} in get_param: {}'.format(type(param), old))
def convert_get_attr(self, old):
attr = old['get_attr']
if not isinstance(attr, list):
raise RuntimeError(
'Attributes for get_attr convertsion must of type list.')
if 'MinViableMtu' in attr:
return self.to_j2_var('min_viable_mtu')
elif 'MinViableMtuBondApi' in attr:
return self.to_j2_var('min_viable_mtu_ctlplane')
elif 'MinViableMtuBondData' in attr:
return self.to_j2_var('min_viable_mtu_dataplane')
else:
print('WARNING - only MinViableMtu and combined '
'MinViableMtuBondApi + MinViableMtuBondData attribute can '
'be converted. Please review the Ansible Jinja2 template '
'and convert this manually.')
return 'NEED MANUAL CONVERSION: {}'.format(str(old))
def convert_list_join(self, list_join_attrs):
to_join = list()
for x in list_join_attrs[1]:
to_join.append(self.recursive_convert(x))
return list_join_attrs[0].join(to_join)
def convert_list_concat(self, list_concat_attrs):
ansible_concat_tpl = "{} | flatten"
to_concatenate = list()
if not isinstance(list_concat_attrs, list):
raise RuntimeError('list_concat_args must be a list')
for x in list_concat_attrs:
to_concatenate.append(self.recursive_convert(x))
to_concatenate = self.normalize_complex(to_concatenate)
new = ansible_concat_tpl.format(to_concatenate)
return self.to_j2_var(new)
def convert_list_concat_unique(self, list_concat_unique_attrs):
ansible_concat_unique_tpl = "{} | flatten | unique"
to_concatenate = list()
if not isinstance(list_concat_unique_attrs, list):
raise RuntimeError('list_concat_unique_attrs must be a list')
for x in list_concat_unique_attrs:
to_concatenate.append(self.recursive_convert(x))
to_concatenate = self.normalize_complex(to_concatenate)
new = ansible_concat_unique_tpl.format(to_concatenate)
return self.to_j2_var(new)
@staticmethod
def convert_unsupported(old, fn):
print('WARNING - can not convert unsupported heat intrinsic function '
' {}. Please review the Ansible Jinja2 template and convert '
'this manually.'.format(fn))
return ('UNSUPPORTED HEAT INTRINSIC FUNCTION {} '
'REQUIRES MANUAL CONVERSION {}'.format(fn, old))
def recursive_convert(self, old):
if isinstance(old, (bool, str, int)):
new = old
elif isinstance(old, dict):
for fn in UNSUPPORTED_HEAT_INTRINSIC_FUNCTIONS:
if fn in old:
return self.convert_unsupported(old, fn)
if 'get_param' in old:
return self.convert_get_param(old)
elif 'get_attr' in old:
return self.convert_get_attr(old)
elif 'list_join' in old:
return self.convert_list_join(old['list_join'])
elif 'list_concat_unique' in old:
return self.convert_list_concat_unique(
old['list_concat_unique'])
else:
new = collections.OrderedDict()
for k, v in old.items():
if isinstance(v, (bool, str, int)):
new[k] = v
elif isinstance(v, (list, dict)):
new[k] = self.recursive_convert(v)
else:
raise RuntimeError(
'Unexpected Type {} for key: {}'.format(
type(v), k))
elif isinstance(old, list):
new = list()
for x in old:
new.append(self.recursive_convert(x))
else:
raise RuntimeError(
'Unknown type {} for recursive convert'.format(type(old)))
return new
def convert_template(self, template):
with open(template, 'r') as f:
heat_tpl = yaml.safe_load(f.read())
resources = set(heat_tpl['resources'].keys())
if not resources.issubset({'OsNetConfigImpl',
'MinViableMtu',
'MinViableMtuBondApi',
'MinViableMtuBondData'}):
msg = ('Only OsNetConfigImpl and MinViableMtu resoureces '
'supported. Found resources: {}'.format(resources))
raise RuntimeError(msg)
net_config_res = heat_tpl['resources'].get('OsNetConfigImpl')
mtu_header = None
if heat_tpl['resources'].get('MinViableMtu'):
mtu_header = 'single'
elif (heat_tpl['resources'].get('MinViableMtuBondApi')
and heat_tpl['resources'].get('MinViableMtuBondData')):
mtu_header = 'dual'
if not net_config_res:
raise RuntimeError('OsNetConfigImpl resource not found in '
'template.')
net_config_res_props = net_config_res['properties']
if net_config_res['type'] == 'OS::Heat::Value':
h_net_conf = net_config_res_props['value']
elif net_config_res['type'] == 'OS::Heat::SoftwareConfig':
h_net_conf = net_config_res_props['config']['str_replace'][
'params']['$network_config']['network_config']
else:
raise RuntimeError('No network config found in provided template.')
j2_config = collections.OrderedDict({'network_config': []})
j2_net_conf = j2_config['network_config']
j2_net_conf.extend(self.recursive_convert(h_net_conf))
j2_header = None
if self.hard_coded_parameters:
j2_header = (
'{# The values of the following custom heat parameters was '
'hard-coded into this template:\n')
for param in self.hard_coded_parameters:
j2_header += ' * {}\n'.format(param)
j2_header += (
'To parameterize use the {{role.name}}ExtraGroupVars THT '
'interface and update the template to use an ansible var.\n'
'#}\n')
return j2_config, mtu_header, j2_header
def write_j2_template(j2_template, j2_config, mtu_header, j2_header):
"""Write the Jinja2 template file
This is done in three steps because the YAML dumper insists
to add quotes:
write the template file
read the template file
re-write it with quotes removed
"""
with open(j2_template, 'w') as f:
if j2_header:
f.write(j2_header)
f.write('---\n')
if mtu_header:
if mtu_header == 'single':
f.write(MIN_VIABLE_MTU_HEADER)
elif mtu_header == 'dual':
f.write(DUAL_MIN_VIABLE_MTU_HEADER)
yaml.dump(j2_config, f, TemplateDumper, width=256,
default_flow_style=False)
with open(j2_template, 'r') as f:
data = f.read()
# Remove quote before jinja2 var reference
data = data.replace('\'{{', '{{')
# Remove quote after jinja2 var reference
data = data.replace('}}\'', '}}')
# Remove quote before value imported from stack environment
data = data.replace('\'' + QUOTE_FIX, '')
# Remove quote after value imported from stack environment
data = data.replace(QUOTE_FIX + '\'', '')
# Remove quote before value imported from stack environment
data = data.replace('\'\'' + DBL_QUOTE_FIX, '')
# Remove quote after value imported from stack environment
data = data.replace(DBL_QUOTE_FIX + '\'\'', '')
# Remove quote_fix string
data = data.replace(QUOTE_FIX, '')
data = data.replace(DBL_QUOTE_FIX, '')
with open(j2_template, 'w') as f:
f.write(data)
def validate_files(opts, template, networks_file, j2_template):
if not os.path.exists(template):
raise RuntimeError('Template file not found {}.'.format(template))
if not os.path.isfile(template):
raise RuntimeError('Template {} is not a file.'.format(template))
if not os.path.exists(networks_file):
raise RuntimeError('Networks file not found, {}.'.format(
networks_file))
if not os.path.isfile(networks_file):
raise RuntimeError('Networks file {} is not a file.'.format(
networks_file))
if os.path.exists(j2_template) and not opts.yes:
raise RuntimeError('Ansible Jinja2 template {} already exists'.format(
j2_template))
if os.path.exists(j2_template) and not os.path.isfile(j2_template):
raise RuntimeError('Existing {} is not a file.'.format(j2_template))
pass
def get_stack_environment(stack_name):
try:
conn = openstack.connect('undercloud')
stack = conn.orchestration.find_stack(stack_name)
if not stack:
print('INFO: Heat stack {} not found.'.format(stack_name))
return {}
stack_env = conn.orchestration.get_stack_environment(stack)
except Exception as e:
print('ERROR: Unable to get stack environment.')
raise e
return stack_env
def main():
opts = parse_opts(sys.argv)
template = os.path.abspath(opts.template)
networks_file = os.path.abspath(opts.networks_file)
j2_template = os.path.splitext(template)[0] + '.j2'
validate_files(opts, template, networks_file, j2_template)
stack_env = get_stack_environment(opts.stack)
converter = ConvertToAnsibleJ2(stack_env, networks_file)
j2_config, mtu_header, j2_header = converter.convert_template(template)
write_j2_template(j2_template, j2_config, mtu_header, j2_header)
if __name__ == '__main__':
main()

0
tools/tests/__init__.py

344
tools/tests/nic_config_convert_samples/heat_templates/2-linux-bonds-vlans-controller.yaml

@ -0,0 +1,344 @@
heat_template_version: rocky
description: >
Software Config to drive os-net-config with 2 Linux bonds. One bond is on a
bridge with VLANs attached for the Controller role.
parameters:
ControlPlaneIp:
default: ''
description: IP address/subnet on the ctlplane network
type: string
ControlPlaneSubnetCidr:
default: ''
description: >
The subnet CIDR of the control plane network. (The parameter is
automatically resolved from the ctlplane subnet's cidr attribute.)
type: string
ControlPlaneDefaultRoute:
default: ''
description: The default route of the control plane network. (The parameter
is automatically resolved from the ctlplane subnet's gateway_ip attribute.)
type: string
ControlPlaneStaticRoutes:
default: []
description: >
Routes for the ctlplane network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
ControlPlaneMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the network.
(The parameter is automatically resolved from the ctlplane network's mtu attribute.)
type: number
StorageIpSubnet:
default: ''
description: IP address/subnet on the storage network
type: string
StorageNetworkVlanID:
default: 1
description: Vlan ID for the storage network traffic.
type: number
StorageMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
Storage network.
type: number
StorageInterfaceRoutes:
default: []
description: >
Routes for the storage network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
StorageMgmtIpSubnet:
default: ''
description: IP address/subnet on the storage_mgmt network
type: string
StorageMgmtNetworkVlanID:
default: 1
description: Vlan ID for the storage_mgmt network traffic.
type: number
StorageMgmtMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
StorageMgmt network.
type: number
StorageMgmtInterfaceRoutes:
default: []
description: >
Routes for the storage_mgmt network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
InternalApiIpSubnet:
default: ''
description: IP address/subnet on the internal_api network
type: string
InternalApiNetworkVlanID:
default: 1
description: Vlan ID for the internal_api network traffic.
type: number
InternalApiMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
InternalApi network.
type: number
InternalApiInterfaceRoutes:
default: []
description: >
Routes for the internal_api network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
TenantIpSubnet:
default: ''
description: IP address/subnet on the tenant network
type: string
TenantNetworkVlanID:
default: 1
description: Vlan ID for the tenant network traffic.
type: number
TenantMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
Tenant network.
type: number
TenantInterfaceRoutes:
default: []
description: >
Routes for the tenant network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
ExternalIpSubnet:
default: ''
description: IP address/subnet on the external network
type: string
ExternalNetworkVlanID:
default: 1
description: Vlan ID for the external network traffic.
type: number
ExternalMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
External network.
type: number
ExternalInterfaceDefaultRoute:
default: ''
description: default route for the external network
type: string
ExternalInterfaceRoutes:
default: []
description: >
Routes for the external network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
DnsServers: # Override this via parameter_defaults
default: []
description: >
DNS servers to use for the Overcloud (2 max for some implementations).
If not set the nameservers configured in the ctlplane subnet's
dns_nameservers attribute will be used.
type: comma_delimited_list
DnsSearchDomains: # Override this via parameter_defaults
default: []
description: A list of DNS search domains to be added (in order) to resolv.conf.
type: comma_delimited_list
BondInterfaceOvsOptions:
default: bond_mode=active-backup
description: 'The ovs_options or bonding_options string for the bond
interface. Set things like lacp=active and/or bond_mode=balance-slb
for OVS bonds or like mode=4 for Linux bonds using this option.'
type: string
resources:
MinViableMtuBondApi:
# This resource resolves the minimum viable MTU for interfaces, bonds and
# bridges that carry multiple VLANs. Each VLAN may have different MTU. The
# bridge, bond or interface must have an MTU to allow the VLAN with the
# largest MTU.
type: OS::Heat::Value
properties:
type: number
value:
yaql:
expression: $.data.max()
data:
- {get_param: ControlPlaneMtu}
- {get_param: StorageMtu}
- {get_param: StorageMgmtMtu}
- {get_param: InternalApiMtu}
- {get_param: ExternalMtu}
MinViableMtuBondData:
# This resource resolves the minimum viable MTU for interfaces, bonds and
# bridges that carry multiple VLANs. Each VLAN may have different MTU. The
# bridge, bond or interface must have an MTU to allow the VLAN with the
# largest MTU.
type: OS::Heat::Value
properties:
type: number
value:
yaql:
expression: $.data.max()
data:
- {get_param: ControlPlaneMtu}
- {get_param: TenantMtu}
OsNetConfigImpl:
type: OS::Heat::SoftwareConfig
properties:
group: script
config:
str_replace:
template:
get_file: ../../scripts/run-os-net-config.sh
params:
$network_config:
network_config:
- type: interface
name: nic1
mtu:
get_param: ControlPlaneMtu
use_dhcp: false
addresses:
- ip_netmask:
list_join:
- /
- - get_param: ControlPlaneIp
- get_param: ControlPlaneSubnetCidr
routes:
list_concat_unique:
- get_param: ControlPlaneStaticRoutes
- type: linux_bond
name: bond_api
mtu:
get_attr: [MinViableMtuBondApi, value]
use_dhcp: false
bonding_options:
get_param: BondInterfaceOvsOptions
dns_servers:
get_param: DnsServers
domain:
get_param: DnsSearchDomains
members:
- type: interface
name: nic2
mtu:
get_attr: [MinViableMtuBondApi, value]
primary: true
- type: interface
name: nic3
mtu:
get_attr: [MinViableMtuBondApi, value]
- type: vlan
device: bond_api
mtu:
get_param: StorageMtu
vlan_id:
get_param: StorageNetworkVlanID
addresses:
- ip_netmask:
get_param: StorageIpSubnet
routes:
list_concat_unique:
- get_param: StorageInterfaceRoutes
- type: vlan
device: bond_api
mtu:
get_param: StorageMgmtMtu
vlan_id:
get_param: StorageMgmtNetworkVlanID
addresses:
- ip_netmask:
get_param: StorageMgmtIpSubnet
routes:
list_concat_unique:
- get_param: StorageMgmtInterfaceRoutes
- type: vlan
device: bond_api
mtu:
get_param: InternalApiMtu
vlan_id:
get_param: InternalApiNetworkVlanID
addresses:
- ip_netmask:
get_param: InternalApiIpSubnet
routes:
list_concat_unique:
- get_param: InternalApiInterfaceRoutes
- type: vlan
device: bond_api
mtu:
get_param: ExternalMtu
vlan_id:
get_param: ExternalNetworkVlanID
addresses:
- ip_netmask:
get_param: ExternalIpSubnet
routes:
list_concat_unique:
- get_param: ExternalInterfaceRoutes
- - default: true
next_hop:
get_param: ExternalInterfaceDefaultRoute
- type: ovs_bridge
name: bridge_name
dns_servers:
get_param: DnsServers
members:
- type: linux_bond
name: bond-data
mtu:
get_attr: [MinViableMtuBondData, value]
bonding_options:
get_param: BondInterfaceOvsOptions
members:
- type: interface
name: nic4
mtu:
get_attr: [MinViableMtuBondData, value]
primary: true
- type: interface
name: nic5
mtu:
get_attr: [MinViableMtuBondData, value]
- type: vlan
device: bond-data
mtu:
get_param: TenantMtu
vlan_id:
get_param: TenantNetworkVlanID
addresses:
- ip_netmask:
get_param: TenantIpSubnet
routes:
list_concat_unique:
- get_param: TenantInterfaceRoutes
outputs:
OS::stack_id:
description: The OsNetConfigImpl resource.
value:
get_resource: OsNetConfigImpl

298
tools/tests/nic_config_convert_samples/heat_templates/bond-vlans-controller.yaml

@ -0,0 +1,298 @@
heat_template_version: rocky
description: >
Software Config to drive os-net-config with 2 bonded nics on a bridge with VLANs attached for the Controller role.
parameters:
ControlPlaneIp:
default: ''
description: IP address/subnet on the ctlplane network
type: string
ControlPlaneSubnetCidr:
default: ''
description: >
The subnet CIDR of the control plane network. (The parameter is
automatically resolved from the ctlplane subnet's cidr attribute.)
type: string
ControlPlaneDefaultRoute:
default: ''
description: The default route of the control plane network. (The parameter
is automatically resolved from the ctlplane subnet's gateway_ip attribute.)
type: string
ControlPlaneStaticRoutes:
default: []
description: >
Routes for the ctlplane network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
ControlPlaneMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the network.
(The parameter is automatically resolved from the ctlplane network's mtu attribute.)
type: number
StorageIpSubnet:
default: ''
description: IP address/subnet on the storage network
type: string
StorageNetworkVlanID:
default: 1
description: Vlan ID for the storage network traffic.
type: number
StorageMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
Storage network.
type: number
StorageInterfaceRoutes:
default: []
description: >
Routes for the storage network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
StorageMgmtIpSubnet:
default: ''
description: IP address/subnet on the storage_mgmt network
type: string
StorageMgmtNetworkVlanID:
default: 1
description: Vlan ID for the storage_mgmt network traffic.
type: number
StorageMgmtMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
StorageMgmt network.
type: number
StorageMgmtInterfaceRoutes:
default: []
description: >
Routes for the storage_mgmt network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
InternalApiIpSubnet:
default: ''
description: IP address/subnet on the internal_api network
type: string
InternalApiNetworkVlanID:
default: 1
description: Vlan ID for the internal_api network traffic.
type: number
InternalApiMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
InternalApi network.
type: number
InternalApiInterfaceRoutes:
default: []
description: >
Routes for the internal_api network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
TenantIpSubnet:
default: ''
description: IP address/subnet on the tenant network
type: string
TenantNetworkVlanID:
default: 1
description: Vlan ID for the tenant network traffic.
type: number
TenantMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
Tenant network.
type: number
TenantInterfaceRoutes:
default: []
description: >
Routes for the tenant network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
ExternalIpSubnet:
default: ''
description: IP address/subnet on the external network
type: string
ExternalNetworkVlanID:
default: 1
description: Vlan ID for the external network traffic.
type: number
ExternalMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
External network.
type: number
ExternalInterfaceDefaultRoute:
default: ''
description: default route for the external network
type: string
ExternalInterfaceRoutes:
default: []
description: >
Routes for the external network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
DnsServers: # Override this via parameter_defaults
default: []
description: >
DNS servers to use for the Overcloud (2 max for some implementations).
If not set the nameservers configured in the ctlplane subnet's
dns_nameservers attribute will be used.
type: comma_delimited_list
DnsSearchDomains: # Override this via parameter_defaults
default: []
description: A list of DNS search domains to be added (in order) to resolv.conf.
type: comma_delimited_list
BondInterfaceOvsOptions:
default: bond_mode=active-backup
description: 'The ovs_options or bonding_options string for the bond
interface. Set things like lacp=active and/or bond_mode=balance-slb
for OVS bonds or like mode=4 for Linux bonds using this option.'
type: string
resources:
MinViableMtu:
# This resource resolves the minimum viable MTU for interfaces, bonds and
# bridges that carry multiple VLANs. Each VLAN may have different MTU. The
# bridge, bond or interface must have an MTU to allow the VLAN with the
# largest MTU.
type: OS::Heat::Value
properties:
type: number
value:
yaql:
expression: $.data.max()
data:
- {get_param: ControlPlaneMtu}
- {get_param: StorageMtu}
- {get_param: StorageMgmtMtu}
- {get_param: InternalApiMtu}
- {get_param: TenantMtu}
- {get_param: ExternalMtu}
OsNetConfigImpl:
type: OS::Heat::SoftwareConfig
properties:
group: script
config:
str_replace:
template:
get_file: ../../scripts/run-os-net-config.sh
params:
$network_config:
network_config:
- type: interface
name: nic1
mtu:
get_param: ControlPlaneMtu
use_dhcp: false
addresses:
- ip_netmask:
list_join:
- /
- - get_param: ControlPlaneIp
- get_param: ControlPlaneSubnetCidr
routes:
list_concat_unique:
- get_param: ControlPlaneStaticRoutes
- type: ovs_bridge
name: bridge_name
dns_servers:
get_param: DnsServers
domain:
get_param: DnsSearchDomains
members:
- type: ovs_bond
name: bond1
mtu:
get_attr: [MinViableMtu, value]
ovs_options:
get_param: BondInterfaceOvsOptions
members:
- type: interface
name: nic2
mtu:
get_attr: [MinViableMtu, value]
primary: true
- type: interface
name: nic3
mtu:
get_attr: [MinViableMtu, value]
- type: vlan
mtu:
get_param: StorageMtu
vlan_id:
get_param: StorageNetworkVlanID
addresses:
- ip_netmask:
get_param: StorageIpSubnet
routes:
list_concat_unique:
- get_param: StorageInterfaceRoutes
- type: vlan
mtu:
get_param: StorageMgmtMtu
vlan_id:
get_param: StorageMgmtNetworkVlanID
addresses:
- ip_netmask:
get_param: StorageMgmtIpSubnet
routes:
list_concat_unique:
- get_param: StorageMgmtInterfaceRoutes
- type: vlan
mtu:
get_param: InternalApiMtu
vlan_id:
get_param: InternalApiNetworkVlanID
addresses:
- ip_netmask:
get_param: InternalApiIpSubnet
routes:
list_concat_unique:
- get_param: InternalApiInterfaceRoutes
- type: vlan
mtu:
get_param: TenantMtu
vlan_id:
get_param: TenantNetworkVlanID
addresses:
- ip_netmask:
get_param: TenantIpSubnet
routes:
list_concat_unique:
- get_param: TenantInterfaceRoutes
- type: vlan
mtu:
get_param: ExternalMtu
vlan_id:
get_param: ExternalNetworkVlanID
addresses:
- ip_netmask:
get_param: ExternalIpSubnet
routes:
list_concat_unique:
- get_param: ExternalInterfaceRoutes
- - default: true
next_hop:
get_param: ExternalInterfaceDefaultRoute
outputs:
OS::stack_id:
description: The OsNetConfigImpl resource.
value:
get_resource: OsNetConfigImpl

239
tools/tests/nic_config_convert_samples/heat_templates/complex.yaml

@ -0,0 +1,239 @@
heat_template_version: rocky
parameters:
# Supernets
StorageSupernet:
type: string
StorageMgmtSupernet:
type: string
InternalApiSupernet:
type: string
TenantSupernet:
type: string
ExternalSupernet:
type: string
# Default Routes
ControlPlaneDefaultRoute:
type: string
ControlPlaneEdge1DefaultRoute:
type: string
ControlPlaneEdge2DefaultRoute:
type: string
StorageInterfaceDefaultRoute:
type: string
StorageEdge1InterfaceDefaultRoute:
type: string
StorageEdge2InterfaceDefaultRoute:
type: string
StorageMgmtInterfaceDefaultRoute:
type: string
StorageMgmtEdge1InterfaceDefaultRoute:
type: string
StorageMgmtEdge2InterfaceDefaultRoute:
type: string
InternalApiInterfaceDefaultRoute:
type: string
InternalApiEdge1InterfaceDefaultRoute:
type: string
InternalApiEdge2InterfaceDefaultRoute:
type: string
TenantInterfaceDefaultRoute:
type: string
TenantEdge1InterfaceDefaultRoute:
type: string
TenantEdge2InterfaceDefaultRoute:
type: string
ExternalInterfaceDefaultRoute:
type: string
# IP subnets
StorageIpSubnet:
default: ''
type: string
StorageEdge1IpSubnet:
default: ''
type: string
StorageEdge2IpSubnet:
default: ''
type: string
StorageMgmtIpSubnet:
default: ''
type: string
StorageMgmtEdge1IpSubnet:
default: ''
type: string
StorageMgmtEdge2IpSubnet:
default: ''
type: string
InternalApiIpSubnet:
default: ''
type: string
InternalApiEdge1IpSubnet:
default: ''
type: string
InternalApiEdge2IpSubnet:
default: ''
type: string
TenantIpSubnet:
default: ''
type: string
TenantEdge1IpSubnet:
default: ''
type: string
TenantEdge2IpSubnet:
default: ''
type: string
ExternalIpSubnet:
default: ''
type: string
ManagementIpSubnet:
default: ''
type: string
# VLAN IDs
StorageNetworkVlanID:
default: 121
type: number
StorageEdge1NetworkVlanID:
default: 131
type: number
StorageEdge2NetworkVlanID:
default: 141
type: number
StorageMgmtNetworkVlanID:
default: 122
type: number
StorageMgmtEdge1NetworkVlanID:
default: 132
type: number
StorageMgmtEdge2NetworkVlanID:
default: 142
type: number
InternalApiNetworkVlanID:
default: 123
type: number
InternalApiEdge1NetworkVlanID:
default: 133
type: number
InternalApiEdge2NetworkVlanID:
default: 143
type: number
TenantNetworkVlanID:
default: 124
type: number
TenantEdge1NetworkVlanID:
default: 134
type: number
TenantEdge2NetworkVlanID:
default: 144
type: number
ExternalNetworkVlanID:
default: 200
type: number
ManagementNetworkVlanID:
default: 300
type: number
# Subnet CIDR
ControlPlaneSubnetCidr:
type: string
ControlPlaneEdge1SubnetCidr:
type: string
ControlPlaneEdge2SubnetCidr:
type: string
ControlPlaneIp:
type: string
DnsServers:
type: comma_delimited_list
# EC2 metadata server IPs
EC2MetadataIp:
type: string
Edge1EC2MetadataIp:
type: string
Edge2EC2MetadataIp:
type: string
resources:
OsNetConfigImpl:
type: OS::Heat::SoftwareConfig
properties:
group: script
config:
str_replace:
template:
get_file: /usr/share/openstack-tripleo-heat-templates/network/scripts/run-os-net-config.sh
params:
$network_config:
network_config:
- type: interface
name: nic1
use_dhcp: false
dns_servers:
get_param: DnsServers
addresses:
- ip_netmask:
list_join:
- /
- - get_param: ControlPlaneIp
- get_param: ControlPlaneEdge1SubnetCidr
routes:
routes:
list_concat_unique:
- default: true
next_hop:
get_param: ControlPlaneEdge1DefaultRoute
- - ip_netmask: 192.168.254.0/24
next_hop: 192.168.24.1
- - ip_netmask: 169.254.169.254/32
next_hop:
get_param: Edge1EC2MetadataIp
- type: ovs_bridge
name: br-ex
use_dhcp: false
members:
- type: interface
name: nic2
primary: true
- type: vlan
vlan_id:
get_param: StorageEdge1NetworkVlanID
addresses:
- ip_netmask:
get_param: StorageEdge1IpSubnet
routes:
- ip_netmask:
get_param: StorageSupernet
next_hop:
get_param: StorageEdge1InterfaceDefaultRoute
- type: vlan
vlan_id:
get_param: InternalApiEdge1NetworkVlanID
addresses:
- ip_netmask:
get_param: InternalApiEdge1IpSubnet
routes:
- ip_netmask:
get_param: InternalApiSupernet
next_hop:
get_param: InternalApiEdge1InterfaceDefaultRoute
- type: vlan
vlan_id:
get_param: TenantEdge1NetworkVlanID
addresses:
- ip_netmask:
get_param: TenantEdge1IpSubnet
routes:
- ip_netmask:
get_param: TenantSupernet
next_hop:
get_param: TenantEdge1InterfaceDefaultRoute
outputs:
OS::stack_id:
description: The OsNetConfigImpl resource.
value:
get_resource: OsNetConfigImpl

280
tools/tests/nic_config_convert_samples/heat_templates/multiple-nics-vlans-controller.yaml

@ -0,0 +1,280 @@
heat_template_version: rocky
description: >
Software Config to drive os-net-config to configure multiple interfaces for the Controller role.
parameters:
ControlPlaneIp:
default: ''
description: IP address/subnet on the ctlplane network
type: string
ControlPlaneSubnetCidr:
default: ''
description: >
The subnet CIDR of the control plane network. (The parameter is
automatically resolved from the ctlplane subnet's cidr attribute.)
type: string
ControlPlaneDefaultRoute:
default: ''
description: The default route of the control plane network. (The parameter
is automatically resolved from the ctlplane subnet's gateway_ip attribute.)
type: string
ControlPlaneStaticRoutes:
default: []
description: >
Routes for the ctlplane network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
ControlPlaneMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the network.
(The parameter is automatically resolved from the ctlplane network's mtu attribute.)
type: number
StorageIpSubnet:
default: ''
description: IP address/subnet on the storage network
type: string
StorageMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
Storage network.
type: number
StorageInterfaceRoutes:
default: []
description: >
Routes for the storage network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
StorageMgmtIpSubnet:
default: ''
description: IP address/subnet on the storage_mgmt network
type: string
StorageMgmtMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
StorageMgmt network.
type: number
StorageMgmtInterfaceRoutes:
default: []
description: >
Routes for the storage_mgmt network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
InternalApiIpSubnet:
default: ''
description: IP address/subnet on the internal_api network
type: string
InternalApiMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
InternalApi network.
type: number
InternalApiInterfaceRoutes:
default: []
description: >
Routes for the internal_api network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
TenantIpSubnet:
default: ''
description: IP address/subnet on the tenant network
type: string
TenantMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
Tenant network.
type: number
TenantInterfaceRoutes:
default: []
description: >
Routes for the tenant network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
ExternalIpSubnet:
default: ''
description: IP address/subnet on the external network
type: string
ExternalMtu:
default: 1500
description: The maximum transmission unit (MTU) size(in bytes) that is
guaranteed to pass through the data path of the segments in the
External network.
type: number
ExternalInterfaceDefaultRoute:
default: ''
description: default route for the external network
type: string
ExternalInterfaceRoutes:
default: []
description: >
Routes for the external network traffic.
JSON route e.g. [{'destination':'10.0.0.0/16', 'nexthop':'10.0.0.1'}]
Unless the default is changed, the parameter is automatically resolved
from the subnet host_routes attribute.
type: json
DnsServers: # Override this via parameter_defaults
default: []
description: >
DNS servers to use for the Overcloud (2 max for some implementations).
If not set the nameservers configured in the ctlplane subnet's
dns_nameservers attribute will be used.
type: comma_delimited_list
DnsSearchDomains: # Override this via parameter_defaults
default: []
description: A list of DNS search domains to be added (in order) to resolv.conf.
type: comma_delimited_list
resources:
OsNetConfigImpl:
type: OS::Heat::SoftwareConfig
properties:
group: script
config:
str_replace:
template:
get_file: ../../scripts/run-os-net-config.sh
params:
$network_config:
network_config:
- type: interface
name: nic1
mtu:
get_param: ControlPlaneMtu
use_dhcp: false
dns_servers:
get_param: DnsServers
domain:
get_param: DnsSearchDomains
addresses:
- ip_netmask:
list_join:
- /
- - get_param: ControlPlaneIp
- get_param: ControlPlaneSubnetCidr
routes:
list_concat_unique:
- get_param: ControlPlaneStaticRoutes
- type: interface
name: nic2
mtu:
get_param: StorageMtu
use_dhcp: false
- type: vlan
device: nic2
mtu:
get_param: StorageMtu
vlan_id:
get_param: StorageNetworkVlanID
addresses:
- ip_netmask:
get_param: StorageIpSubnet
routes:
list_concat_unique:
- get_param: StorageInterfaceRoutes
- type: interface
name: nic5
mtu:
get_param: StorageMgmtMtu
use_dhcp: false
- type: vlan
device: nic5
mtu:
get_param: StorageMgmtMtu
vlan_id:
get_param: StorageMgmtNetworkVlanID
addresses:
- ip_netmask:
get_param: StorageMgmtIpSubnet
routes:
list_concat_unique:
- get_param: StorageMgmtInterfaceRoutes
- type: interface
name: nic8
mtu:
get_param: InternalApiMtu
use_dhcp: false
- type: vlan
device: nic8
mtu:
get_param: InternalApiMtu
vlan_id:
get_param: InternalApiNetworkVlanID
addresses:
- ip_netmask:
get_param: InternalApiIpSubnet
routes:
list_concat_unique:
- get_param: InternalApiInterfaceRoutes
- type: ovs_bridge
name: br-tenant