Add Ironic multitenancy support

This patch adds support to deploy Ironic with separate Neutron network
for provisioning baremetal instances.

  * Add NetworkDeploymentSerializer100 as Ironic multitenancy is supported
    from Newton, and we will backport this till stable/newton.
  * Update network scheme generation to create 'vlan' baremetal network,
    assign IPs to Ironic conductors from this network, make them
    accessible from baremetal servers.
  * Add new checkbox at 'Openstack Settings/Additional components' tab
    which allows to define if separate provisioning network should be used
    during deployment. This is a trigger to switch ironic deployment to
    multitenancy case. If not selected old behaviour is kept, 'flat'
    network is used. The checkbox is shown only when Ironic component is
    enabled.

Change-Id: I861a8b3b046202526d6a856c9f2dca2cfaddc887
Related-Bug: #1588380
This commit is contained in:
Vasyl Saienko 2017-03-17 12:06:47 +02:00
parent 738066cdca
commit 9dabe06369
7 changed files with 422 additions and 35 deletions

View File

@ -104,6 +104,7 @@ DEFAULT_BRIDGES_NAMES = Enum(
'br-aux',
'br-baremetal',
'br-ironic',
'br-bm-phy',
names=(
'br_fw_admin',
'br_storage',
@ -115,7 +116,8 @@ DEFAULT_BRIDGES_NAMES = Enum(
'br_mesh',
'br_aux',
'br_baremetal',
'br_ironic'
'br_ironic',
'br_bm_phy'
)
)

View File

@ -27,6 +27,7 @@ from nailgun.extensions.network_manager.serializers.base \
import NetworkDeploymentSerializer
from nailgun import consts
from nailgun.consts import DEFAULT_BRIDGES_NAMES as DBN
from nailgun.db import db
from nailgun.db.sqlalchemy import models
from nailgun.logger import logger
@ -602,7 +603,7 @@ class NeutronNetworkDeploymentSerializer61(
(consts.NEUTRON_SEGMENT_TYPES.gre,
consts.NEUTRON_SEGMENT_TYPES.tun):
transformations.append(
cls.add_bridge(consts.DEFAULT_BRIDGES_NAMES.br_mesh))
cls.add_bridge(DBN.br_mesh))
@classmethod
def add_bond_interface(cls, transformations, iface, nets_by_ifaces, nm):
@ -792,14 +793,14 @@ class NeutronNetworkDeploymentSerializer61(
"""
transformations.append(cls.add_bridge(
consts.DEFAULT_BRIDGES_NAMES.br_prv,
DBN.br_prv,
provider=consts.NEUTRON_L23_PROVIDERS.ovs))
if not prv_base_ep:
prv_base_ep = consts.DEFAULT_BRIDGES_NAMES.br_aux
prv_base_ep = DBN.br_aux
transformations.append(cls.add_bridge(prv_base_ep))
transformations.append(cls.add_patch(
bridges=[consts.DEFAULT_BRIDGES_NAMES.br_prv, prv_base_ep],
bridges=[DBN.br_prv, prv_base_ep],
provider=consts.NEUTRON_L23_PROVIDERS.ovs,
mtu=65000))
@ -830,7 +831,7 @@ class NeutronNetworkDeploymentSerializer70(
@classmethod
def is_valid_non_default_bridge_name(cls, name):
"""Validate bridge name for non-default network."""
if name in consts.DEFAULT_BRIDGES_NAMES:
if name in DBN:
return False
return bool(cls.RE_BRIDGE_NAME.match(name))
@ -1285,12 +1286,12 @@ class GenerateL23Mixin80(object):
def generate_l2(cls, cluster):
l2 = super(GenerateL23Mixin80, cls).generate_l2(cluster)
l2["phys_nets"]["physnet1"] = {
"bridge": consts.DEFAULT_BRIDGES_NAMES.br_floating,
"bridge": DBN.br_floating,
"vlan_range": None
}
if objects.Cluster.is_component_enabled(cluster, 'ironic'):
l2["phys_nets"]["physnet-ironic"] = {
"bridge": consts.DEFAULT_BRIDGES_NAMES.br_ironic,
"bridge": DBN.br_ironic,
"vlan_range": None
}
return l2
@ -1355,23 +1356,23 @@ class NeutronNetworkDeploymentSerializer80(
def get_network_to_endpoint_mapping(cls, node):
mapping = {
consts.NETWORKS.fuelweb_admin:
consts.DEFAULT_BRIDGES_NAMES.br_fw_admin,
DBN.br_fw_admin,
consts.NETWORKS.storage:
consts.DEFAULT_BRIDGES_NAMES.br_storage,
DBN.br_storage,
consts.NETWORKS.management:
consts.DEFAULT_BRIDGES_NAMES.br_mgmt}
DBN.br_mgmt}
# roles can be assigned to br-ex only in case it has a public IP
if objects.Node.should_have_public_with_ip(node):
mapping[consts.NETWORKS.public] = \
consts.DEFAULT_BRIDGES_NAMES.br_ex
DBN.br_ex
if node.cluster.network_config.segmentation_type in \
(consts.NEUTRON_SEGMENT_TYPES.gre,
consts.NEUTRON_SEGMENT_TYPES.tun):
mapping[consts.NETWORKS.private] = \
consts.DEFAULT_BRIDGES_NAMES.br_mesh
DBN.br_mesh
if objects.Cluster.is_component_enabled(node.cluster, 'ironic'):
mapping[consts.NETWORKS.baremetal] = \
consts.DEFAULT_BRIDGES_NAMES.br_baremetal
DBN.br_baremetal
mapping.update(cls.get_node_non_default_bridge_mapping(node))
return mapping
@ -1383,12 +1384,12 @@ class NeutronNetworkDeploymentSerializer80(
is_public, prv_base_ep))
if objects.Cluster.is_component_enabled(node.cluster, 'ironic'):
transformations.insert(0, cls.add_bridge(
consts.DEFAULT_BRIDGES_NAMES.br_baremetal))
DBN.br_baremetal))
transformations.append(cls.add_bridge(
consts.DEFAULT_BRIDGES_NAMES.br_ironic, provider='ovs'))
DBN.br_ironic, provider='ovs'))
transformations.append(cls.add_patch(
bridges=[consts.DEFAULT_BRIDGES_NAMES.br_ironic,
consts.DEFAULT_BRIDGES_NAMES.br_baremetal],
bridges=[DBN.br_ironic,
DBN.br_baremetal],
provider='ovs'))
return transformations
@ -1489,9 +1490,9 @@ class DPDKSerializerMixin90(object):
vendor_specific = {'datapath_type': 'netdev'}
if node.cluster.network_config.segmentation_type == \
consts.NEUTRON_SEGMENT_TYPES.vlan:
br_name = consts.DEFAULT_BRIDGES_NAMES.br_prv
br_name = DBN.br_prv
else:
br_name = consts.DEFAULT_BRIDGES_NAMES.br_mesh
br_name = DBN.br_mesh
vlan_id = objects.NetworkGroup.get_node_network_by_name(
node, 'private').vlan_start
if vlan_id:
@ -1653,18 +1654,142 @@ class NeutronNetworkTemplateSerializer90(
pass
class NeutronNetworkTemplateSerializer110(
class NeutronNetworkTemplateSerializer100(
NeutronNetworkTemplateSerializer90
):
pass
class NeutronNetworkDeploymentSerializer110(
class NeutronNetworkDeploymentSerializer100(
NeutronNetworkDeploymentSerializer90
):
@classmethod
def _is_ironic_multitenancy_enabled(cls, cluster):
"""Check if ironic multitenancy is enabled."""
return bool(
utils.get_in(
cluster.attributes.editable,
'ironic_settings',
'ironic_provision_network',
'value'
)
)
@classmethod
def _generate_baremetal_network(cls, cluster):
ng = objects.NetworkGroup.get_from_node_group_by_name(
objects.Cluster.get_default_group(cluster).id, 'baremetal')
network_type = 'flat'
segment_id = None
shared = True
if cls._is_ironic_multitenancy_enabled(cluster):
network_type = 'vlan'
segment_id = ng.vlan_start
shared = False
return {
"L3": {
"subnet": ng.cidr,
"nameservers": cluster.network_config.dns_nameservers,
"gateway": cluster.network_config.baremetal_gateway,
"floating": utils.join_range(
cluster.network_config.baremetal_range),
"enable_dhcp": True
},
"L2": {
"network_type": network_type,
"segment_id": segment_id,
"router_ext": False,
"physnet": "physnet-ironic"
},
"tenant": objects.Cluster.get_creds(
cluster)['tenant']['value'],
"shared": shared
}
@classmethod
def generate_l2(cls, cluster):
l2 = super(NeutronNetworkDeploymentSerializer100,
cls).generate_l2(cluster)
if (objects.Cluster.is_component_enabled(cluster, 'ironic') and
cls._is_ironic_multitenancy_enabled(cluster)):
ng = objects.NetworkGroup.get_from_node_group_by_name(
objects.Cluster.get_default_group(cluster).id, 'baremetal')
vlan_range = "{0}:{0}".format(ng.vlan_start)
l2["phys_nets"]["physnet-ironic"] = {
"bridge": DBN.br_ironic,
"vlan_range": vlan_range
}
return l2
@classmethod
def generate_transformations(cls, node, nm, nets_by_ifaces, is_public,
prv_base_ep):
transformations = (
super(NeutronNetworkDeploymentSerializer100, cls)
.generate_transformations(
node, nm, nets_by_ifaces, is_public, prv_base_ep))
if (objects.Cluster.is_component_enabled(node.cluster, 'ironic') and
cls._is_ironic_multitenancy_enabled(node.cluster)):
transformations.insert(0, {'action': 'add-br',
'name': DBN.br_bm_phy})
netgroup = nm.get_network_by_netname('baremetal',
node.network_data)
br_bm_phy_sub = '{0}.{1}'.format(DBN.br_bm_phy, netgroup['vlan'])
bm_phy_configured = False
for t in transformations:
action = t.get('action')
# Each transformation can be matched with the only one
# condition at a time.
if (action == 'add-patch' and
t.get('bridges') == [DBN.br_ironic,
DBN.br_baremetal]):
t['bridges'] = [DBN.br_ironic,
DBN.br_bm_phy]
elif (action == 'add-port' and
t.get('name') == netgroup['dev']):
transformations.append(
cls.add_patch(bridges=[DBN.br_bm_phy,
t.get('bridge')]))
bm_phy_configured = True
elif (action == 'add-port' and
t.get('bridge') == DBN.br_baremetal):
t['name'] = br_bm_phy_sub
# This is possible when only baremetal network is assigned to
# interface. Add physical interface to br-bm-phy in this case.
if not bm_phy_configured:
transformations.append(cls.add_port(netgroup['dev'],
DBN.br_bm_phy))
return transformations
@classmethod
def generate_network_scheme(cls, node, networks):
attrs = super(NeutronNetworkDeploymentSerializer100,
cls).generate_network_scheme(node, networks)
if (objects.Cluster.is_component_enabled(node.cluster, 'ironic') and
cls._is_ironic_multitenancy_enabled(node.cluster)):
attrs['endpoints'][DBN.br_ironic] = {'IP': 'none'}
return attrs
class NeutronNetworkTemplateSerializer110(
NeutronNetworkTemplateSerializer100
):
pass
class NeutronNetworkDeploymentSerializer110(
NeutronNetworkDeploymentSerializer100,
):
@classmethod
def generate_transformations_by_segmentation_type(
cls, node, nm, transformations, prv_base_ep, nets_by_ifaces
cls, node, nm, transformations, prv_base_ep, nets_by_ifaces
):
(super(NeutronNetworkDeploymentSerializer110, cls)
.generate_transformations_by_segmentation_type(

View File

@ -948,6 +948,20 @@
description: "Name for Mongo replication set"
weight: 30
type: "text"
ironic_settings:
metadata:
label: "Ironic Settings"
weight: 40
group: "openstack_services"
restrictions:
- condition: "settings:additional_components.ironic.value == false"
action: "hide"
ironic_provision_network:
value: false
label: "Use separate provisioning network"
description: "When selected Ironic instance will be provisioned in separate network."
weight: 10
type: "checkbox"
additional_components:
metadata:
label: "Additional Components"

View File

@ -809,7 +809,17 @@ class DeploymentLCMSerializer(DeploymentHASerializer90):
return serialized_node
class DeploymentLCMSerializer110(DeploymentLCMSerializer):
class DeploymentLCMSerializer100(DeploymentLCMSerializer):
@classmethod
def get_net_provider_serializer(cls, cluster):
if cluster.network_config.configuration_template:
return neutron_serializers.NeutronNetworkTemplateSerializer100
else:
return neutron_serializers.NeutronNetworkDeploymentSerializer100
class DeploymentLCMSerializer110(DeploymentLCMSerializer100):
@classmethod
def get_net_provider_serializer(cls, cluster):
@ -897,6 +907,7 @@ def serialize_for_lcm(cluster, nodes,
ignore_customized=False, skip_extensions=False):
serializers_map = {
'default': DeploymentLCMSerializer,
'10.0': DeploymentLCMSerializer100,
'11.0': DeploymentLCMSerializer110,
}

View File

@ -157,6 +157,9 @@ class InstallationInfo(object):
WhiteListRule(('murano_settings',
'murano_glance_artifacts_plugin', 'value'),
'murano_glance_artifacts_plugin', None),
WhiteListRule(('ironic_settings',
'ironic_provision_network', 'value'),
'ironic_provision_network', None),
WhiteListRule(('workloads_collector', 'enabled', 'value'),
'workloads_collector_enabled', None),

View File

@ -0,0 +1,239 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, 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.
from nailgun import consts
from nailgun import objects
from nailgun.orchestrator import deployment_serializers
from nailgun.orchestrator.deployment_serializers import \
deployment_info_to_legacy
from nailgun.test.integration.test_orchestrator_serializer import \
BaseDeploymentSerializer
from nailgun.test.integration import test_orchestrator_serializer_90
from nailgun.extensions.network_manager.serializers.neutron_serializers \
import NeutronNetworkDeploymentSerializer100
from nailgun.extensions.network_manager.serializers.neutron_serializers \
import NeutronNetworkTemplateSerializer100
class TestSerializer100Mixin(object):
env_version = 'newton-10.0'
task_deploy = True
@classmethod
def create_serializer(cls, cluster):
return deployment_serializers.DeploymentLCMSerializer100()
@classmethod
def _get_serializer(cluster):
return deployment_serializers.DeploymentLCMSerializer100()
@staticmethod
def _get_plugins_names(plugins):
"""Plugins names for LCM serializers
Single out <name> since plugin data may contain
<scripts>, <repositories>, <whatever> as well.
:param nodes: array of plugins data
:returns: singled out names of plugins
"""
return [plugin['name'] for plugin in plugins]
def _setup_cluster_with_ironic(self, ironic_provision_network,
separate_interface=False):
self.env._set_additional_component(self.cluster, 'ironic', True)
if ironic_provision_network:
objects.Cluster.patch_attributes(
self.cluster,
{'editable': {
'ironic_settings': {
'ironic_provision_network': {
'value': True}}}})
node = self.env.create_nodes_w_interfaces_count(
1, 4, cluster_id=self.cluster.id,
roles=['controller', 'ironic'])[0]
if separate_interface:
nic_1 = node.nic_interfaces[0]
nic_4 = node.nic_interfaces[3]
nets_1 = nic_1.assigned_networks_list
nets_4 = nic_4.assigned_networks_list
for i, net in enumerate(nets_1):
if net['name'] == 'baremetal':
nets_4.append(nets_1.pop(i))
break
objects.NIC.assign_networks(nic_1, nets_1)
objects.NIC.assign_networks(nic_4, nets_4)
objects.Cluster.prepare_for_deployment(self.cluster)
class TestSerializeInterfaceDriversData100(
TestSerializer100Mixin,
test_orchestrator_serializer_90.TestSerializeInterfaceDriversData90
):
pass
class TestDeploymentTasksSerialization100(
TestSerializer100Mixin,
test_orchestrator_serializer_90.TestDeploymentTasksSerialization90
):
pass
class TestNetworkTemplateSerializer100(
TestSerializer100Mixin,
test_orchestrator_serializer_90.TestNetworkTemplateSerializer90
):
pass
class TestNetworkDeploymentSerializer100(
TestSerializer100Mixin,
BaseDeploymentSerializer,
):
legacy_serializer = NeutronNetworkDeploymentSerializer100
template_serializer = NeutronNetworkTemplateSerializer100
def setUp(self, *args):
super(TestNetworkDeploymentSerializer100, self).setUp()
self.cluster = self.env.create(
release_kwargs={'version': self.env_version},
cluster_kwargs={
'mode': consts.CLUSTER_MODES.ha_compact,
'net_provider': consts.CLUSTER_NET_PROVIDERS.neutron,
'net_segment_type': consts.NEUTRON_SEGMENT_TYPES.vlan})
self.serializer = self.create_serializer(self.cluster)
def test_baremetal_neutron_attrs_flat(self):
self._setup_cluster_with_ironic(ironic_provision_network=False)
serialized_for_astute = self.serializer.serialize(
self.cluster, self.cluster.nodes)
serialized_for_astute = deployment_info_to_legacy(
serialized_for_astute)
for node in serialized_for_astute:
expected_network = {
"network_type": "flat",
"segment_id": None,
"router_ext": False,
"physnet": "physnet-ironic"
}
self.assertEqual(expected_network, node['quantum_settings']
['predefined_networks']['baremetal']['L2'])
self.assertIn("physnet-ironic",
node['quantum_settings']['L2']['phys_nets'])
self.assertEqual(consts.DEFAULT_BRIDGES_NAMES.br_ironic,
(node['quantum_settings']['L2']['phys_nets']
["physnet-ironic"]["bridge"]))
self.assertEqual(None, (node['quantum_settings']['L2']['phys_nets']
["physnet-ironic"]["vlan_range"]))
def test_baremetal_neutron_attrs_vlan(self):
self._setup_cluster_with_ironic(ironic_provision_network=True)
serialized_for_astute = self.serializer.serialize(
self.cluster, self.cluster.nodes)
serialized_for_astute = deployment_info_to_legacy(
serialized_for_astute)
for node in serialized_for_astute:
expected_network = {
"network_type": "vlan",
"segment_id": 104,
"router_ext": False,
"physnet": "physnet-ironic"
}
self.assertEqual(expected_network, node['quantum_settings']
['predefined_networks']['baremetal']['L2'])
self.assertIn("physnet-ironic",
node['quantum_settings']['L2']['phys_nets'])
self.assertEqual(consts.DEFAULT_BRIDGES_NAMES.br_ironic,
(node['quantum_settings']['L2']['phys_nets']
["physnet-ironic"]["bridge"]))
self.assertEqual('104:104',
(node['quantum_settings']['L2']['phys_nets']
["physnet-ironic"]["vlan_range"]))
def test_baremetal_transformations_flat(self):
self._setup_cluster_with_ironic(ironic_provision_network=False)
serialized_for_astute = self.serializer.serialize(
self.cluster, self.cluster.nodes)
serialized_for_astute = deployment_info_to_legacy(
serialized_for_astute)
net_tr = serialized_for_astute[0]['network_scheme']['transformations']
expected_actions = [
{'action': 'add-br', 'name': 'br-baremetal'},
{'action': 'add-port', 'bridge': 'br-baremetal',
'name': 'eth0.104'},
{'action': 'add-br', 'name': 'br-ironic', 'provider': 'ovs'},
{'action': 'add-patch', 'bridges': ['br-ironic', 'br-baremetal'],
'provider': 'ovs'}]
for element in expected_actions:
self.assertIn(element, net_tr)
def test_baremetal_transformations_vlan(self):
self._setup_cluster_with_ironic(ironic_provision_network=True)
serialized_for_astute = self.serializer.serialize(
self.cluster, self.cluster.nodes)
serialized_for_astute = deployment_info_to_legacy(
serialized_for_astute)
net_tr = serialized_for_astute[0]['network_scheme']['transformations']
expected_actions = [
{'action': 'add-br', 'name': 'br-bm-phy'},
{'action': 'add-br', 'name': 'br-baremetal'},
{'action': 'add-port', 'bridge': 'br-baremetal',
'name': 'br-bm-phy.104'},
{'action': 'add-br', 'name': 'br-ironic', 'provider': 'ovs'},
{'action': 'add-patch', 'bridges': ['br-ironic', 'br-bm-phy'],
'provider': 'ovs'},
{'action': 'add-patch', 'bridges': ['br-bm-phy', 'br-fw-admin']}]
not_expected = [
{'action': 'add-port', 'bridge': 'br-bm-phy', 'name': u'eth0'}]
for element in expected_actions:
self.assertIn(element, net_tr)
for elemnet in not_expected:
self.assertNotIn(elemnet, net_tr)
def test_baremetal_transformations_vlan_baremetal_on_separate_nic(self):
self._setup_cluster_with_ironic(ironic_provision_network=True,
separate_interface=True)
serialized_for_astute = self.serializer.serialize(
self.cluster, self.cluster.nodes)
serialized_for_astute = deployment_info_to_legacy(
serialized_for_astute)
net_tr = serialized_for_astute[0]['network_scheme']['transformations']
expected_actions = [
{'action': 'add-br', 'name': 'br-bm-phy'},
{'action': 'add-br', 'name': 'br-baremetal'},
{'action': 'add-port', 'bridge': 'br-baremetal',
'name': 'br-bm-phy.104'},
{'action': 'add-br', 'name': 'br-ironic', 'provider': 'ovs'},
{'action': 'add-patch', 'bridges': ['br-ironic', 'br-bm-phy'],
'provider': 'ovs'},
{'action': 'add-port', 'bridge': 'br-bm-phy', 'name': u'eth3'}]
not_expected = [
{'action': 'add-patch', 'bridges': ['br-bm-phy', 'br-fw-admin']}]
for element in expected_actions:
self.assertIn(element, net_tr)
for elemnet in not_expected:
self.assertNotIn(elemnet, net_tr)

View File

@ -16,7 +16,7 @@
from nailgun import consts
from nailgun.orchestrator import deployment_serializers
from nailgun.test.integration import test_orchestrator_serializer_90
from nailgun.test.integration import test_orchestrator_serializer_100
class TestSerializer110Mixin(object):
@ -45,22 +45,15 @@ class TestSerializer110Mixin(object):
return [plugin['name'] for plugin in plugins]
class TestDeploymentAttributesSerialization110(
class TestNetworkDeploymentSerializer110(
TestSerializer110Mixin,
test_orchestrator_serializer_90.TestDeploymentAttributesSerialization90
):
pass
class TestDeploymentLCMSerialization110(
TestSerializer110Mixin,
test_orchestrator_serializer_90.TestDeploymentLCMSerialization90
test_orchestrator_serializer_100.TestNetworkDeploymentSerializer100
):
pass
class TestSerializeInterfaceDriversData110(
TestSerializer110Mixin,
test_orchestrator_serializer_90.TestSerializeInterfaceDriversData90
test_orchestrator_serializer_100.TestSerializeInterfaceDriversData100
):
pass