Merge "Introduce role/instance 'networks' key"

This commit is contained in:
Zuul 2021-01-04 19:49:26 +00:00 committed by Gerrit Code Review
commit 16292d3155
3 changed files with 283 additions and 26 deletions

View File

@ -14,7 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from copy import deepcopy as dcopy
import jsonschema import jsonschema
import metalsmith import metalsmith
@ -44,6 +44,18 @@ _NIC_SCHEMA = {
'additionalProperties': False 'additionalProperties': False
} }
_NETWORK_SCHEMA = {
'type': 'object',
'properties': {
'network': {'type': 'string'},
'port': {'type': 'string'},
'fixed_ip': {'type': 'string'},
'subnet': {'type': 'string'},
'vif': {'type': 'boolean'}
},
'additionalProperties': False
}
_INSTANCE_SCHEMA = { _INSTANCE_SCHEMA = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
@ -57,10 +69,10 @@ _INSTANCE_SCHEMA = {
'image': _IMAGE_SCHEMA, 'image': _IMAGE_SCHEMA,
'name': {'type': 'string'}, 'name': {'type': 'string'},
'netboot': {'type': 'boolean'}, 'netboot': {'type': 'boolean'},
'nics': { 'nics': {'type': 'array',
'type': 'array', 'items': _NIC_SCHEMA},
'items': _NIC_SCHEMA 'networks': {'type': 'array',
}, 'items': _NETWORK_SCHEMA},
'passwordless_sudo': {'type': 'boolean'}, 'passwordless_sudo': {'type': 'boolean'},
'profile': {'type': 'string'}, 'profile': {'type': 'string'},
'provisioned': {'type': 'boolean'}, 'provisioned': {'type': 'boolean'},
@ -84,6 +96,21 @@ _INSTANCES_SCHEMA = {
} }
"""JSON schema of the instances list.""" """JSON schema of the instances list."""
_no_nics = dcopy(_INSTANCE_SCHEMA)
_no_networks = dcopy(_INSTANCE_SCHEMA)
del _no_nics['properties']['nics']
del _no_networks['properties']['networks']
_ROLE_DEFAULTS_SCHEMA = {
'anyOf': [_no_nics, _no_networks]
}
"""JSON schema of the role defaults."""
_INSTANCES_INPUT_SCHEMA = {
'type': 'array',
'items': {'anyOf': [_no_nics, _no_networks]},
}
"""JSON schema of the instances input."""
_ROLES_INPUT_SCHEMA = { _ROLES_INPUT_SCHEMA = {
'type': 'array', 'type': 'array',
@ -93,8 +120,8 @@ _ROLES_INPUT_SCHEMA = {
'name': {'type': 'string'}, 'name': {'type': 'string'},
'hostname_format': {'type': 'string'}, 'hostname_format': {'type': 'string'},
'count': {'type': 'integer', 'minimum': 0}, 'count': {'type': 'integer', 'minimum': 0},
'defaults': _INSTANCE_SCHEMA, 'defaults': _ROLE_DEFAULTS_SCHEMA,
'instances': _INSTANCES_SCHEMA, 'instances': _INSTANCES_INPUT_SCHEMA,
}, },
'additionalProperties': False, 'additionalProperties': False,
'required': ['name'], 'required': ['name'],
@ -110,19 +137,27 @@ class BaremetalDeployException(Exception):
def expand(roles, stack_name, expand_provisioned=True, default_image=None, def expand(roles, stack_name, expand_provisioned=True, default_image=None,
default_network=None, user_name=None, ssh_public_keys=None): default_network=None, user_name=None, ssh_public_keys=None):
def _remove_vif_key(nets):
for net in nets:
net.pop('vif', None)
for role in roles: for role in roles:
role.setdefault('defaults', {}) defaults = role.setdefault('defaults', {})
if default_image: if default_image:
role['defaults'].setdefault('image', default_image) defaults.setdefault('image', default_image)
if default_network:
role['defaults'].setdefault('nics', default_network)
if ssh_public_keys: if ssh_public_keys:
role['defaults'].setdefault('ssh_public_keys', ssh_public_keys) defaults.setdefault('ssh_public_keys', ssh_public_keys)
if user_name: if user_name:
role['defaults'].setdefault('user_name', user_name) defaults.setdefault('user_name', user_name)
if default_network:
default_networks = defaults.setdefault('networks', [])
default_networks.extend([x for x in default_network
if x not in default_networks])
for inst in role.get('instances', []): for inst in role.get('instances', []):
for k, v in role['defaults'].items(): merge_networks_defaults(defaults, inst)
for k, v in defaults.items():
inst.setdefault(k, v) inst.setdefault(k, v)
# Set the default hostname now for duplicate hostname # Set the default hostname now for duplicate hostname
@ -177,8 +212,8 @@ def expand(roles, stack_name, expand_provisioned=True, default_image=None,
hostname_format) hostname_format)
# ensure each instance has a unique non-empty hostname # ensure each instance has a unique non-empty hostname
# and a hostname map entry. Also build a list of indexes # and a hostname map entry and add nics entry for vif networks.
# for unprovisioned instances # Also build a list of indexes for unprovisioned instances
index = 0 index = 0
for inst in role_instances: for inst in role_instances:
provisioned = inst.get('provisioned', True) provisioned = inst.get('provisioned', True)
@ -203,6 +238,12 @@ def expand(roles, stack_name, expand_provisioned=True, default_image=None,
unprovisioned_indexes.append( unprovisioned_indexes.append(
potential_gen_names[hostname]) potential_gen_names[hostname])
vif_networks = [x for x in dcopy(inst.get('networks', []))
if x.get('vif')]
if vif_networks:
_remove_vif_key(vif_networks)
inst.setdefault('nics', vif_networks)
if unprovisioned_indexes: if unprovisioned_indexes:
parameter_defaults['%sRemovalPolicies' % name] = [{ parameter_defaults['%sRemovalPolicies' % name] = [{
'resource_list': unprovisioned_indexes 'resource_list': unprovisioned_indexes
@ -222,7 +263,7 @@ def expand(roles, stack_name, expand_provisioned=True, default_image=None,
parameter_defaults['%sCount' % name] = ( parameter_defaults['%sCount' % name] = (
provisioned_count) provisioned_count)
validate_instances(instances) validate_instances(instances, _INSTANCES_SCHEMA)
if expand_provisioned: if expand_provisioned:
env = {'parameter_defaults': parameter_defaults} env = {'parameter_defaults': parameter_defaults}
else: else:
@ -230,8 +271,27 @@ def expand(roles, stack_name, expand_provisioned=True, default_image=None,
return instances, env return instances, env
def merge_networks_defaults(defaults, instance):
d_networks = defaults.get('networks', [])
i_networks = instance.get('networks', [])
if not d_networks:
return
i_dict = {x['network']: x for x in i_networks}
d_dict = {x['network']: x for x in d_networks}
# only merge networks not already defined on the instance
for key in d_dict:
if key not in i_dict:
i_networks.append(d_dict[key])
# only set non-empty networks value on the instance
if i_networks:
instance['networks'] = i_networks
def check_existing(instances, provisioner, baremetal): def check_existing(instances, provisioner, baremetal):
validate_instances(instances) validate_instances(instances, _INSTANCES_SCHEMA)
# Due to the name shadowing we should import other way # Due to the name shadowing we should import other way
import importlib import importlib
@ -319,8 +379,8 @@ def build_hostname(hostname_format, index, stack):
return gen_name return gen_name
def validate_instances(instances): def validate_instances(instances, schema):
jsonschema.validate(instances, _INSTANCES_SCHEMA) jsonschema.validate(instances, schema)
hostnames = set() hostnames = set()
names = set() names = set()
for inst in instances: for inst in instances:
@ -366,7 +426,7 @@ def validate_roles(roles):
raise ValueError("%s: cannot specify provisioned in defaults" raise ValueError("%s: cannot specify provisioned in defaults"
% name) % name)
if 'instances' in item: if 'instances' in item:
validate_instances(item['instances']) validate_instances(item['instances'], _INSTANCES_INPUT_SCHEMA)
def get_source(instance): def get_source(instance):

View File

@ -99,6 +99,7 @@ options:
suboptions: dict suboptions: dict
default: default:
- network: ctlplane - network: ctlplane
vif: true
default_image: default_image:
description: description:
- Default image - Default image
@ -191,11 +192,13 @@ EXAMPLES = '''
defaults: defaults:
image: image:
href: overcloud-full href: overcloud-full
networks: []
- name: Compute - name: Compute
count: 3 count: 3
defaults: defaults:
image: image:
href: overcloud-full href: overcloud-full
networks: []
state: present state: present
stack_name: overcloud stack_name: overcloud
register: tripleo_baremetal_instances register: tripleo_baremetal_instances

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import jsonschema
import metalsmith import metalsmith
from unittest import mock from unittest import mock
from openstack import exceptions as sdk_exc from openstack import exceptions as sdk_exc
@ -56,10 +57,45 @@ class TestBaremetalDeployUtils(base.TestCase):
) )
) )
def test_merge_networks_defaults(self):
# Network defined only in role defaults is appended
defaults = {'networks': [{'network': 'role_net'}]}
instance = {'networks': [{'network': 'instance_net'}]}
bd.merge_networks_defaults(defaults, instance)
self.assertEqual({'networks': [{'network': 'instance_net'},
{'network': 'role_net'}]}, instance)
# Network defined in both role defaults and instance is not appended
instance = {'networks': [{'network': 'instance_net'},
{'network': 'role_net'}]}
bd.merge_networks_defaults(defaults, instance)
self.assertEqual({'networks': [{'network': 'instance_net'},
{'network': 'role_net'}]}, instance)
# Network defined in role defaults and in instance with richer data
# is not appended.
instance = {'networks': [{'network': 'instance_net'},
{'network': 'role_net', 'port': 'port_uuid'}]}
bd.merge_networks_defaults(defaults, instance)
self.assertEqual({'networks': [{'network': 'instance_net'},
{'network': 'role_net',
'port': 'port_uuid'}]}, instance)
# Network defined in role defaults with richer data compared to the
# instance is not appended.
defaults = {'networks': [{'network': 'role_net',
'subnet': 'subnet_name'}]}
instance = {'networks': [{'network': 'instance_net'},
{'network': 'role_net'}]}
bd.merge_networks_defaults(defaults, instance)
self.assertEqual({'networks': [{'network': 'instance_net'},
{'network': 'role_net'}]}, instance)
class TestExpandRoles(base.TestCase): class TestExpandRoles(base.TestCase):
default_image = {'href': 'overcloud-full'} default_image = {'href': 'overcloud-full'}
default_network = [{'network': 'ctlplane', 'vif': True}]
def test_simple(self): def test_simple(self):
roles = [ roles = [
@ -93,6 +129,149 @@ class TestExpandRoles(base.TestCase):
}, },
environment['parameter_defaults']) environment['parameter_defaults'])
def test_default_network(self):
roles = [
{'name': 'Compute'},
{'name': 'Controller'},
]
instances, environment = bd.expand(
roles, 'overcloud', True, self.default_image, self.default_network
)
self.assertEqual(
[
{'hostname': 'overcloud-novacompute-0',
'image': {'href': 'overcloud-full'},
'networks': [{'network': 'ctlplane', 'vif': True}],
'nics': [{'network': 'ctlplane'}]},
{'hostname': 'overcloud-controller-0',
'image': {'href': 'overcloud-full'},
'networks': [{'network': 'ctlplane', 'vif': True}],
'nics': [{'network': 'ctlplane'}]},
],
instances)
def test_networks_set_no_default_network(self):
roles = [
{'name': 'Compute',
'defaults': {
'networks': [
{'network': 'some_net', 'vif': True},
]}
},
{'name': 'Controller',
'defaults': {
'networks': [
{'network': 'some_net', 'vif': True},
]}
},
]
instances, environment = bd.expand(
roles, 'overcloud', True, self.default_image, None
)
self.assertEqual(
[
{'hostname': 'overcloud-novacompute-0',
'image': {'href': 'overcloud-full'},
'networks': [{'network': 'some_net', 'vif': True}],
'nics': [{'network': 'some_net'}]},
{'hostname': 'overcloud-controller-0',
'image': {'href': 'overcloud-full'},
'networks': [{'network': 'some_net', 'vif': True}],
'nics': [{'network': 'some_net'}]},
],
instances)
def test_networks_set_default_appended(self):
roles = [
{'name': 'Compute',
'defaults': {
'networks': [
{'network': 'foo', 'subnet': 'foo_subnet'},
]}
},
{'name': 'Controller',
'defaults': {
'networks': [
{'network': 'foo', 'subnet': 'foo_subnet'},
]}
},
]
instances, environment = bd.expand(
roles, 'overcloud', True, self.default_image, self.default_network
)
self.assertEqual(
[
{'hostname': 'overcloud-novacompute-0',
'image': {'href': 'overcloud-full'},
'networks': [{'network': 'foo', 'subnet': 'foo_subnet'},
{'network': 'ctlplane', 'vif': True}],
'nics': [{'network': 'ctlplane'}]},
{'hostname': 'overcloud-controller-0',
'image': {'href': 'overcloud-full'},
'networks': [{'network': 'foo', 'subnet': 'foo_subnet'},
{'network': 'ctlplane', 'vif': True}],
'nics': [{'network': 'ctlplane'}]},
],
instances)
def test_networks_vif_set_default_appended(self):
roles = [
{'name': 'Compute',
'defaults': {
'networks': [
{'network': 'foo', 'subnet': 'foo_subnet', 'vif': True},
]}
},
{'name': 'Controller',
'defaults': {
'networks': [
{'network': 'foo', 'subnet': 'foo_subnet', 'vif': True},
]}
},
]
instances, environment = bd.expand(
roles, 'overcloud', True, self.default_image, self.default_network
)
self.assertEqual(
[
{'hostname': 'overcloud-novacompute-0',
'image': {'href': 'overcloud-full'},
'networks': [
{'network': 'foo', 'subnet': 'foo_subnet', 'vif': True},
{'network': 'ctlplane', 'vif': True}
],
'nics': [{'network': 'foo', 'subnet': 'foo_subnet'},
{'network': 'ctlplane'}],
},
{'hostname': 'overcloud-controller-0',
'image': {'href': 'overcloud-full'},
'networks': [
{'network': 'foo', 'subnet': 'foo_subnet', 'vif': True},
{'network': 'ctlplane', 'vif': True}
],
'nics': [
{'network': 'foo', 'subnet': 'foo_subnet'},
{'network': 'ctlplane'}
]},
],
instances)
def test_networks_nics_are_mutually_exclusive(self):
# Neither 'nics' nor 'networks' - OK
roles = [{'name': 'Compute', 'defaults': {}}]
bd.expand(roles, 'overcloud', True, self.default_image)
# 'networks' but not 'nics' - OK
roles = [{'name': 'Compute', 'defaults': {'networks': []}}]
bd.expand(roles, 'overcloud', True, self.default_image)
# 'nics' but not 'networks' - OK
roles = [{'name': 'Compute', 'defaults': {'nics': []}}]
bd.expand(roles, 'overcloud', True, self.default_image)
# 'networks' and 'nics' - mutually exclusive, Raises ValidationError
roles = [{'name': 'Compute', 'defaults': {'networks': [], 'nics': []}}]
self.assertRaises(
jsonschema.exceptions.ValidationError,
bd.expand, roles, 'overcloud', True, self.default_image)
def test_image_in_defaults(self): def test_image_in_defaults(self):
roles = [{ roles = [{
'name': 'Controller', 'name': 'Controller',
@ -200,15 +379,22 @@ class TestExpandRoles(base.TestCase):
'name': 'Controller', 'name': 'Controller',
'count': 2, 'count': 2,
'defaults': { 'defaults': {
'profile': 'control' 'profile': 'control',
'networks': [
{'network': 'foo', 'subnet': 'foo_subnet'},
]
}, },
'instances': [{ 'instances': [{
'hostname': 'controller-X.example.com', 'hostname': 'controller-X.example.com',
'profile': 'control-X' 'profile': 'control-X',
'networks': [
{'network': 'inst_net', 'fixed_ip': '10.1.1.1'}
]
}, { }, {
'name': 'node-0', 'name': 'node-0',
'traits': ['CUSTOM_FOO'], 'traits': ['CUSTOM_FOO'],
'nics': [{'subnet': 'leaf-2'}]}, 'networks': [{'network': 'some_net', 'subnet': 'leaf-2',
'vif': True}]},
]}, ]},
] ]
instances, environment = bd.expand( instances, environment = bd.expand(
@ -222,12 +408,20 @@ class TestExpandRoles(base.TestCase):
'image': {'href': 'overcloud-full'}}, 'image': {'href': 'overcloud-full'}},
{'hostname': 'controller-X.example.com', {'hostname': 'controller-X.example.com',
'image': {'href': 'overcloud-full'}, 'image': {'href': 'overcloud-full'},
'profile': 'control-X'}, 'profile': 'control-X',
'networks': [{'fixed_ip': '10.1.1.1', 'network': 'inst_net'},
{'network': 'foo', 'subnet': 'foo_subnet'}],
},
# Name provides the default for hostname later on. # Name provides the default for hostname later on.
{'name': 'node-0', 'profile': 'control', {'name': 'node-0', 'profile': 'control',
'hostname': 'node-0', 'hostname': 'node-0',
'networks': [
{'network': 'some_net', 'subnet': 'leaf-2', 'vif': True},
{'network': 'foo', 'subnet': 'foo_subnet'},
],
'image': {'href': 'overcloud-full'}, 'image': {'href': 'overcloud-full'},
'traits': ['CUSTOM_FOO'], 'nics': [{'subnet': 'leaf-2'}]}, 'traits': ['CUSTOM_FOO'],
'nics': [{'network': 'some_net', 'subnet': 'leaf-2'}]},
], ],
instances) instances)
self.assertEqual( self.assertEqual(