Networks are considered as shared if they are in different nodegroups, have the same Name, CIDR and Gateway values. No routes are geneated between shared segments of networks. Partial-Bug: #1484008 Partial-Bug: #1473047 Change-Id: Ifeaffd76d52a46d95918527c62c302ad356d2fe5 Blueprint: nodegroups-share-network-parameterstags/8.0
@@ -357,21 +357,31 @@ class NetworkCheck(object): | |||
(Neutron) | |||
""" | |||
# check intersection of address ranges | |||
# between all networks | |||
for ngs in combinations(self.networks, 2): | |||
if ngs[0].get('cidr') and ngs[1].get('cidr'): | |||
cidr1 = netaddr.IPNetwork(ngs[0]['cidr']) | |||
cidr2 = netaddr.IPNetwork(ngs[1]['cidr']) | |||
# check intersection of address ranges between all networks | |||
for ng1, ng2 in combinations(self.networks, 2): | |||
if ng1.get('cidr') and ng2.get('cidr'): | |||
# networks with the same name in different node groups maybe | |||
# considered as one shared network if they have equal CIDRs | |||
# and gateways | |||
if (ng1['group_id'] != ng2['group_id'] and | |||
ng1['group_id'] is not None and | |||
ng2['group_id'] is not None and | |||
ng1['name'] == ng2['name'] and | |||
ng1['gateway'] is not None and | |||
ng1['gateway'] == ng2['gateway'] and | |||
ng1['cidr'] == ng2['cidr']): | |||
continue | |||
cidr1 = netaddr.IPNetwork(ng1['cidr']) | |||
cidr2 = netaddr.IPNetwork(ng2['cidr']) | |||
if self.net_man.is_cidr_intersection(cidr1, cidr2): | |||
self.err_msgs.append( | |||
u"Address space intersection " | |||
u"between networks:\n{0}".format( | |||
", ".join([ngs[0]['name'], ngs[1]['name']]) | |||
", ".join([ng1['name'], ng2['name']]) | |||
) | |||
) | |||
self.result.append({ | |||
"ids": [int(ngs[0]["id"]), int(ngs[1]["id"])], | |||
"ids": [int(ng1["id"]), int(ng2["id"])], | |||
"errors": ["cidr"] | |||
}) | |||
self.expose_error_messages() |
@@ -1469,9 +1469,9 @@ class NetworkManager(object): | |||
def get_networks_not_on_node(cls, node, networks=None): | |||
networks = networks or cls.get_node_networks(node) | |||
node_net = [(n['name'], n['cidr']) | |||
for n in networks if n.get('cidr')] | |||
for n in networks if n.get('cidr')] | |||
all_nets = [(n.name, n.cidr) | |||
for n in node.cluster.network_groups if n.cidr] | |||
for n in node.cluster.network_groups if n.cidr] | |||
admin_net = cls.get_admin_network_group() | |||
all_nets.append((admin_net.name, admin_net.cidr)) |
@@ -132,9 +132,7 @@ class NeutronManager70(AllocateVIPs70Mixin, NeutronManager): | |||
@classmethod | |||
def get_node_networks_with_ips(cls, node): | |||
"""Returns IP and network data (meta, gateway) for each node network. | |||
""" | |||
"""Returns IP, CIDR, meta, gateway for each network on given node.""" | |||
if not node.group_id: | |||
return {} | |||
@@ -149,6 +147,7 @@ class NeutronManager70(AllocateVIPs70Mixin, NeutronManager): | |||
for ng, ip in ngs: | |||
networks[ng.name] = { | |||
'ip': cls.get_ip_w_cidr_prefix_len(ip, ng), | |||
'cidr': ng.cidr, | |||
'meta': ng.meta, | |||
'gateway': ng.gateway | |||
} | |||
@@ -157,6 +156,7 @@ class NeutronManager70(AllocateVIPs70Mixin, NeutronManager): | |||
networks[admin_ng.name] = { | |||
'ip': cls.get_ip_w_cidr_prefix_len( | |||
cls.get_admin_ip_for_node(node.id), admin_ng), | |||
'cidr': admin_ng.cidr, | |||
'meta': admin_ng.meta, | |||
'gateway': admin_ng.gateway | |||
} |
@@ -604,6 +604,18 @@ class NeutronNetworkDeploymentSerializer61( | |||
@classmethod | |||
def generate_routes(cls, node, attrs, nm, netgroup_mapping, netgroups, | |||
networks): | |||
"""Generate static routes for environment with multiple node groups. | |||
Generate static routes for all networks in all node groups where | |||
gateway is set. | |||
:param node: Node instance | |||
:param attrs: deployment attributes hash (is modified in method) | |||
:param nm: Network Manager for current environment | |||
:param netgroup_mapping: endpoint to network name mapping | |||
:param netgroups: hash of network parameters hashes for node | |||
:param networks: sequence of network parameters hashes | |||
:return: None (attrs is modified) | |||
""" | |||
other_nets = nm.get_networks_not_on_node(node, networks) | |||
for ngname, brname in netgroup_mapping: | |||
@@ -1424,6 +1436,37 @@ class NeutronNetworkDeploymentSerializer80( | |||
provider='ovs')) | |||
return transformations | |||
@classmethod | |||
def generate_routes(cls, node, attrs, nm, netgroup_mapping, netgroups, | |||
networks): | |||
"""Generate static routes for environment with multiple node groups. | |||
Generate static routes for all networks in all node groups where | |||
gateway is set. Routes are not generated between shared L3 segments. | |||
:param node: Node instance | |||
:param attrs: deployment attributes hash (is modified in method) | |||
:param nm: Network Manager for current environment | |||
:param netgroup_mapping: endpoint to network name mapping | |||
:param netgroups: hash of network parameters hashes for node | |||
:param networks: sequence of network parameters hashes | |||
:return: None (attrs is modified) | |||
""" | |||
other_nets = nm.get_networks_not_on_node(node, networks) | |||
cidrs_in_use = set(ng['cidr'] for ng in netgroups if 'cidr' in ng) | |||
for ngname, brname in netgroup_mapping: | |||
netgroup = netgroups[ngname] | |||
if netgroup.get('gateway') and netgroup.get('cidr'): | |||
via = netgroup['gateway'] | |||
attrs['endpoints'][brname]['routes'] = [] | |||
for cidr in other_nets.get(ngname, []): | |||
if cidr not in cidrs_in_use: | |||
attrs['endpoints'][brname]['routes'].append({ | |||
'net': cidr, | |||
'via': via | |||
}) | |||
cidrs_in_use.add(cidr) | |||
class NeutronNetworkTemplateSerializer80( | |||
GenerateL23Mixin80, |
@@ -23,12 +23,14 @@ except ImportError: | |||
import mock | |||
import os | |||
import re | |||
import six | |||
import time | |||
import uuid | |||
from datetime import datetime | |||
from functools import partial | |||
from itertools import izip | |||
from netaddr import IPAddress | |||
from netaddr import IPNetwork | |||
from random import randint | |||
@@ -425,6 +427,54 @@ class EnvironmentManager(object): | |||
db().delete(ng) | |||
db().flush() | |||
def setup_networks_for_nodegroup( | |||
self, cluster_id, node_group, cidr_start, floating_ranges=None): | |||
"""Setup networks of particular node group in multi-node-group mode. | |||
:param cluster_id: Cluster Id | |||
:param node_group: NodeGroup instance | |||
:param cidr_start: first two octets of CIDR which are common for all | |||
networks (e.g. "192.168") | |||
:param floating_ranges: Floating IP ranges | |||
:return: response from network setup API request | |||
""" | |||
ng2_networks = { | |||
'fuelweb_admin': {'cidr': '{0}.9.0/24'.format(cidr_start), | |||
'ip_ranges': [['{0}.9.2'.format(cidr_start), | |||
'{0}.9.254'.format(cidr_start)]], | |||
'gateway': '{0}.9.1'.format(cidr_start)}, | |||
'public': {'cidr': '{0}.0.0/24'.format(cidr_start), | |||
'ip_ranges': [['{0}.0.2'.format(cidr_start), | |||
'{0}.0.127'.format(cidr_start)]], | |||
'gateway': '{0}.0.1'.format(cidr_start)}, | |||
'management': {'cidr': '{0}.1.0/24'.format(cidr_start), | |||
'gateway': '{0}.1.1'.format(cidr_start)}, | |||
'storage': {'cidr': '{0}.2.0/24'.format(cidr_start), | |||
'gateway': '{0}.2.1'.format(cidr_start)}, | |||
'private': {'cidr': '{0}.3.0/24'.format(cidr_start), | |||
'gateway': '{0}.3.1'.format(cidr_start)}, | |||
} | |||
netw_ids = set(net.id for net in node_group.networks) | |||
netconfig = self.neutron_networks_get(cluster_id).json_body | |||
for net in netconfig['networks']: | |||
if not net['meta']['notation']: | |||
continue | |||
if net['id'] in netw_ids and net['name'] in ng2_networks: | |||
for pkey, pval in six.iteritems(ng2_networks[net['name']]): | |||
net[pkey] = pval | |||
elif not net['gateway']: | |||
net['ip_ranges'] = [[ | |||
str(IPAddress(IPNetwork(net['cidr'])[2])), | |||
str(IPAddress(IPNetwork(net['cidr'])[254])), | |||
]] | |||
net['gateway'] = str(IPNetwork(net['cidr'])[1]) | |||
net['meta']['use_gateway'] = True | |||
if floating_ranges: | |||
netconfig['networking_parameters']['floating_ranges'] = \ | |||
floating_ranges | |||
resp = self.neutron_networks_put(cluster_id, netconfig) | |||
return resp | |||
def create_plugin(self, api=False, cluster=None, **kwargs): | |||
plugin_data = self.get_default_plugin_metadata(**kwargs) | |||
@@ -15,10 +15,13 @@ | |||
# under the License. | |||
from copy import deepcopy | |||
import mock | |||
import six | |||
from nailgun import consts | |||
from nailgun.db.sqlalchemy import models | |||
from nailgun import objects | |||
from nailgun import rpc | |||
from nailgun.orchestrator.deployment_graph import AstuteGraph | |||
from nailgun.orchestrator.deployment_serializers import \ | |||
@@ -179,6 +182,82 @@ class TestDeploymentAttributesSerialization80( | |||
self.assertIn(expected_patch, transformations) | |||
class TestMultiNodeGroupsSerialization80(BaseDeploymentSerializer): | |||
env_version = '2015.1.0-8.0' | |||
def setUp(self): | |||
super(TestMultiNodeGroupsSerialization80, self).setUp() | |||
cluster = self.env.create( | |||
release_kwargs={'version': self.env_version}, | |||
cluster_kwargs={ | |||
'net_provider': consts.CLUSTER_NET_PROVIDERS.neutron, | |||
'net_segment_type': consts.NEUTRON_SEGMENT_TYPES.vlan} | |||
) | |||
self.env.create_nodes_w_interfaces_count( | |||
nodes_count=3, | |||
if_count=2, | |||
roles=['controller', 'cinder'], | |||
pending_addition=True, | |||
cluster_id=cluster['id']) | |||
self.cluster_db = self.db.query(models.Cluster).get(cluster['id']) | |||
serializer_type = get_serializer_for_cluster(self.cluster_db) | |||
self.serializer = serializer_type(AstuteGraph(self.cluster_db)) | |||
def _add_node_group_with_node(self, cidr_start, node_address): | |||
node_group = self.env.create_node_group( | |||
api=False, cluster_id=self.cluster_db.id, | |||
name='ng_' + cidr_start + '_' + str(node_address)) | |||
with mock.patch.object(rpc, 'cast'): | |||
resp = self.env.setup_networks_for_nodegroup( | |||
cluster_id=self.cluster_db.id, node_group=node_group, | |||
cidr_start=cidr_start) | |||
self.assertEqual(resp.status_code, 200) | |||
self.db.query(models.Task).filter_by( | |||
name=consts.TASK_NAMES.update_dnsmasq | |||
).delete(synchronize_session=False) | |||
self.env.create_nodes_w_interfaces_count( | |||
nodes_count=1, | |||
if_count=2, | |||
roles=['compute'], | |||
pending_addition=True, | |||
cluster_id=self.cluster_db.id, | |||
group_id=node_group.id, | |||
ip='{0}.9.{1}'.format(cidr_start, node_address)) | |||
def _check_routes_count(self, count): | |||
self.prepare_for_deployment(self.cluster_db.nodes) | |||
facts = self.serializer.serialize( | |||
self.cluster_db, self.cluster_db.nodes) | |||
for node in facts: | |||
endpoints = node['network_scheme']['endpoints'] | |||
for name, descr in six.iteritems(endpoints): | |||
if descr['IP'] == 'none': | |||
self.assertNotIn('routes', descr) | |||
else: | |||
self.assertEqual(len(descr['routes']), count) | |||
def test_routes_with_no_shared_networks_2_nodegroups(self): | |||
self._add_node_group_with_node('199.99', 3) | |||
# all networks have different CIDRs | |||
self._check_routes_count(1) | |||
def test_routes_with_no_shared_networks_3_nodegroups(self): | |||
self._add_node_group_with_node('199.99', 3) | |||
self._add_node_group_with_node('199.77', 3) | |||
# all networks have different CIDRs | |||
self._check_routes_count(2) | |||
def test_routes_with_shared_networks_3_nodegroups(self): | |||
self._add_node_group_with_node('199.99', 3) | |||
self._add_node_group_with_node('199.99', 4) | |||
# networks in two racks have equal CIDRs | |||
self._check_routes_count(1) | |||
class TestSerializeInterfaceDriversData80( | |||
TestSerializer80Mixin, | |||
TestSerializeInterfaceDriversData |
@@ -109,7 +109,7 @@ class TestNetworkCheck(BaseIntegrationTest): | |||
checker.check_untagged_intersection) | |||
@patch.object(helpers, 'db') | |||
def test_check_network_address_spaces_intersection(self, mocked_db): | |||
def test_check_network_address_spaces_intersection(self, _): | |||
cluster = self.env.create( | |||
cluster_kwargs={ | |||
'net_provider': consts.CLUSTER_NET_PROVIDERS.nova_network}, | |||
@@ -162,7 +162,7 @@ class TestNetworkCheck(BaseIntegrationTest): | |||
checker.check_network_address_spaces_intersection) | |||
@patch.object(helpers, 'db') | |||
def test_check_public_floating_ranges_intersection(self, mocked_db): | |||
def test_check_public_floating_ranges_intersection(self, _): | |||
checker = NetworkCheck(self.task, {}) | |||
checker.networks = [{'id': 1, | |||
'cidr': '192.168.0.0/24', | |||
@@ -207,14 +207,16 @@ class TestNetworkCheck(BaseIntegrationTest): | |||
'name': 'public', | |||
'gateway': '192.168.0.1', | |||
'ip_ranges': [['192.168.0.10', '192.168.0.40']], | |||
'meta': {'notation': 'cidr'} | |||
'meta': {'notation': 'cidr'}, | |||
'group_id': 1, | |||
}, | |||
{'id': 2, | |||
'cidr': '192.168.0.128/25', | |||
'name': 'public', | |||
'gateway': '192.168.0.129', | |||
'ip_ranges': [['192.168.0.130', '192.168.0.150']], | |||
'meta': {'notation': 'cidr'} | |||
'meta': {'notation': 'cidr'}, | |||
'group_id': 2, | |||
} | |||
] | |||
@@ -229,6 +231,80 @@ class TestNetworkCheck(BaseIntegrationTest): | |||
"192.168.0.160-192.168.0.200 are not in the same public CIDR.", | |||
checker.neutron_check_network_address_spaces_intersection) | |||
@patch.object(helpers, 'db') | |||
def test_check_non_shared_networks_intersection(self, _): | |||
checker = NetworkCheck(self.task, {}) | |||
networks = [ | |||
{'id': 1, | |||
'cidr': '192.168.0.0/25', | |||
'name': consts.NETWORKS.public, | |||
'gateway': '192.168.0.1', | |||
'ip_ranges': [['192.168.0.10', '192.168.0.40']], | |||
'meta': {'notation': consts.NETWORK_NOTATION.ip_ranges}, | |||
'vlan_start': 111, | |||
'group_id': 1, | |||
}, | |||
{'id': 2, | |||
'cidr': '192.168.0.0/25', | |||
'name': consts.NETWORKS.management, | |||
'gateway': '192.168.0.2', | |||
'ip_ranges': [['192.168.0.50', '192.168.0.110']], | |||
'meta': {'notation': consts.NETWORK_NOTATION.ip_ranges}, | |||
'vlan_start': 222, | |||
'group_id': 1, | |||
} | |||
] | |||
# different networks from one node group, different gateways | |||
checker.networks = networks | |||
self.assertRaisesWithMessage( | |||
errors.NetworkCheckError, | |||
"Address space intersection between networks:\n" | |||
"public, management", | |||
checker.neutron_check_network_address_spaces_intersection) | |||
checker = NetworkCheck(self.task, {}) | |||
# different networks from different node groups, different gateways | |||
networks[1]['group_id'] = 2 | |||
checker.networks = networks | |||
self.assertRaisesWithMessage( | |||
errors.NetworkCheckError, | |||
"Address space intersection between networks:\n" | |||
"public, management", | |||
checker.neutron_check_network_address_spaces_intersection) | |||
checker = NetworkCheck(self.task, {}) | |||
# same networks from different node groups, different gateways | |||
networks[1]['name'] = consts.NETWORKS.public | |||
checker.networks = networks | |||
self.assertRaisesWithMessage( | |||
errors.NetworkCheckError, | |||
"Address space intersection between networks:\n" | |||
"public, public", | |||
checker.neutron_check_network_address_spaces_intersection) | |||
checker = NetworkCheck(self.task, {}) | |||
# same networks from different node groups, same gateway | |||
networks[1]['gateway'] = '192.168.0.1' | |||
checker.networks = networks | |||
checker.network_config['floating_ranges'] = [ | |||
['192.168.0.111', '192.168.0.126'] | |||
] | |||
self.assertNotRaises( | |||
errors.NetworkCheckError, | |||
checker.check_configuration) | |||
checker = NetworkCheck(self.task, {}) | |||
# same networks from different node groups, same gateway, | |||
# intersection in ip_ranges | |||
networks[1]['ip_ranges'] = [['192.168.0.30', '192.168.0.60']] | |||
checker.networks = networks | |||
checker.network_config['floating_ranges'] = [ | |||
['192.168.0.111', '192.168.0.126'] | |||
] | |||
self.assertNotRaises( | |||
errors.NetworkCheckError, | |||
checker.check_configuration) | |||
@patch.object(helpers, 'db') | |||
def test_check_vlan_ids_range_and_intersection_failed(self, mocked_db): | |||
checker = NetworkCheck(self.task, {}) |