diff --git a/nailgun/nailgun/network/manager.py b/nailgun/nailgun/network/manager.py index 5fc756cb7e..bf1481f444 100644 --- a/nailgun/nailgun/network/manager.py +++ b/nailgun/nailgun/network/manager.py @@ -1438,11 +1438,54 @@ class NetworkManager(object): return objects.NetworkGroup.create(data) @classmethod - def create_network_groups(cls, cluster, neutron_segment_type, gid=None): - """Method for creation of network groups for cluster. + def ensure_gateways_present_in_default_node_group(cls, cluster): + """Ensure that all networks in default node group have gateways. + It is required for environment with multiple node groups. GWs are added + to all networks that have L3 setup. If some of IP ranges of such + network intersects with new GW, they will be cut. + :param cluster: Cluster instance + :return: None + """ + for network in objects.Cluster.get_default_group(cluster).networks: + if network.meta['notation'] is None or network.meta['use_gateway']: + continue + # add first address of network as a gateway + cidr = IPNetwork(network.cidr) + default_gw_ip = cidr[1] + network.meta['use_gateway'] = True + network.gateway = str(default_gw_ip) + for ip_range_db in network.ip_ranges: + # check that IP ranges do not intersect with GW + # and cut them if they are + if default_gw_ip not in IPRange(ip_range_db.first, + ip_range_db.last): + continue + if ip_range_db.first != ip_range_db.last: + ip_range_db.first = str(cidr[2]) + else: + db.delete(ip_range_db) + # delete intersecting IPs + # TODO(akasatkin): need to reexamine deleting of IPs when + # manual setting of IPs will be allowed + db.query(IPAddr).filter( + IPAddr.ip_addr == network.gateway, + IPAddr.network == network.id + ).delete() + db().flush() + + @classmethod + def create_network_groups(cls, cluster, neutron_segment_type, + node_group_id=None, set_all_gateways=False): + """Create network groups for node group. + + Creates network groups for default node group of cluster if + node_group_id is not supplied. Node group should not contain any + network groups before this. :param cluster: Cluster instance. - :type cluster: instance + :param neutron_segment_type: segmentation type (only for neutron) + :param node_group_id: ID of node group. + :param set_all_gateways: set gateways for all network groups :returns: None """ networks_metadata = cluster.release.networks_metadata @@ -1456,7 +1499,9 @@ class NetworkManager(object): if cls.check_network_restrictions(cluster, net['restrictions']): continue - cls.create_network_group(cluster, net, gid) + if net['notation'] is not None and set_all_gateways: + net['use_gateway'] = True + cls.create_network_group(cluster, net, node_group_id) @classmethod def update_networks(cls, network_configuration): @@ -1924,7 +1969,7 @@ class AllocateVIPs80Mixin(object): if net_group != consts.NETWORKS.fuelweb_admin and \ net_group not in net_names: logger.warning( - "Skip VIP '{0}' whitch is mapped to non-existing" + "Skip VIP '{0}' which is mapped to non-existing" " network '{1}'".format(vip_name, net_group)) continue diff --git a/nailgun/nailgun/objects/node_group.py b/nailgun/nailgun/objects/node_group.py index 877edb1b65..1d5ed6af94 100644 --- a/nailgun/nailgun/objects/node_group.py +++ b/nailgun/nailgun/objects/node_group.py @@ -37,7 +37,13 @@ class NodeGroup(NailgunObject): cluster = Cluster.get_by_uid(new_group.cluster_id) nm = Cluster.get_network_manager(cluster) nst = cluster.network_config.segmentation_type - nm.create_network_groups(cluster, nst, gid=new_group.id) + # We have two node groups here when user adds the first custom + # node group. + if NodeGroupCollection.get_by_cluster_id(cluster.id).count() == 2: + nm.ensure_gateways_present_in_default_node_group(cluster) + nm.create_network_groups( + cluster, neutron_segment_type=nst, node_group_id=new_group.id, + set_all_gateways=True) nm.create_admin_network_group(new_group.cluster_id, new_group.id) except ( errors.OutOfVLANs, diff --git a/nailgun/nailgun/test/base.py b/nailgun/nailgun/test/base.py index a0055d81d0..59aaa9b2c2 100644 --- a/nailgun/nailgun/test/base.py +++ b/nailgun/nailgun/test/base.py @@ -30,7 +30,6 @@ 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 @@ -485,12 +484,6 @@ class EnvironmentManager(object): 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'] = \ diff --git a/nailgun/nailgun/test/integration/test_network_manager.py b/nailgun/nailgun/test/integration/test_network_manager.py index e15e1f4bd2..5498d3418e 100644 --- a/nailgun/nailgun/test/integration/test_network_manager.py +++ b/nailgun/nailgun/test/integration/test_network_manager.py @@ -776,8 +776,12 @@ class TestNetworkManager(BaseNetworkManagerTest): def test_restricted_networks(self): rel = self.env.create_release() - enabled_net = {'name': 'always_enabled', 'restrictions': ['false']} - disabled_net = {'name': 'always_disabled', 'restrictions': ['true']} + enabled_net = {'name': 'always_enabled', + 'notation': None, + 'restrictions': ['false']} + disabled_net = {'name': 'always_disabled', + 'notation': None, + 'restrictions': ['true']} netw_meta = deepcopy(rel.networks_metadata) netw_meta['neutron']['networks'].extend([enabled_net, disabled_net]) rel.networks_metadata = netw_meta diff --git a/nailgun/nailgun/test/unit/test_node_groups.py b/nailgun/nailgun/test/unit/test_node_groups.py index e0c384afac..985803b1f4 100644 --- a/nailgun/nailgun/test/unit/test_node_groups.py +++ b/nailgun/nailgun/test/unit/test_node_groups.py @@ -16,11 +16,13 @@ from mock import patch +import copy import json +import six from nailgun import consts from nailgun.db import db -from nailgun.db.sqlalchemy.models import NetworkGroup +from nailgun.db.sqlalchemy import models from nailgun import objects from nailgun.test.base import BaseIntegrationTest from nailgun.utils import reverse @@ -28,12 +30,17 @@ from nailgun.utils import reverse class TestNodeGroups(BaseIntegrationTest): + segmentation_type = consts.NEUTRON_SEGMENT_TYPES.gre + def setUp(self): super(TestNodeGroups, self).setUp() - self.cluster = self.env.create_cluster( - api=False, - net_provider=consts.CLUSTER_NET_PROVIDERS.neutron, - net_segment_type=consts.NEUTRON_SEGMENT_TYPES.gre + self.cluster = self.env.create( + release_kwargs={'version': '1111-8.0'}, + cluster_kwargs={ + 'api': False, + 'net_provider': consts.CLUSTER_NET_PROVIDERS.neutron, + 'net_segment_type': self.segmentation_type + } ) def test_nodegroup_creation(self): @@ -102,7 +109,8 @@ class TestNodeGroups(BaseIntegrationTest): resp = self.env.create_node_group() response = resp.json_body - nets = db().query(NetworkGroup).filter_by(group_id=response['id']) + nets = db().query(models.NetworkGroup).filter_by( + group_id=response['id']) self.assertEquals(nets.count(), 5) @patch('nailgun.task.task.rpc.cast') @@ -120,7 +128,8 @@ class TestNodeGroups(BaseIntegrationTest): expect_errors=False ) - nets = db().query(NetworkGroup).filter_by(group_id=response['id']) + nets = db().query(models.NetworkGroup).filter_by( + group_id=response['id']) self.assertEquals(nets.count(), 0) def test_nodegroup_vlan_segmentation_type(self): @@ -379,3 +388,144 @@ class TestNodeGroups(BaseIntegrationTest): message = resp.json_body['message'] self.assertEquals(resp.status_code, 400) self.assertRegexpMatches(message, 'Cannot assign node group') + + @patch('nailgun.task.task.rpc.cast') + def test_net_config_is_valid_after_nodegroup_is_created(self, _): + # API validator does not allow to setup networks w/o gateways when + # cluster has multiple node groups. Since now, no additional actions + # are required from user to get valid configuration after new node + # group is created, i.e. this network configuration will pass through + # the API validator. + resp = self.env.create_node_group() + self.assertEquals(resp.status_code, 201) + + config = self.env.neutron_networks_get(self.cluster.id).json_body + resp = self.env.neutron_networks_put(self.cluster.id, config) + self.assertEqual(resp.status_code, 200) + + def test_all_networks_have_gw_after_nodegroup_is_created(self): + resp = self.env.create_node_group() + self.assertEquals(resp.status_code, 201) + + for network in self.cluster.network_groups: + if network.meta['notation'] is not None: + self.assertTrue(network.meta['use_gateway']) + self.assertIsNotNone(network.gateway) + + def test_intersecting_ip_deleted_after_nodegroup_is_created(self): + net_roles = copy.copy( + self.env.clusters[0].release.network_roles_metadata) + net_roles.append({ + 'id': 'stor/vip', + 'default_mapping': consts.NETWORKS.storage, + 'properties': { + 'subnet': True, + 'gateway': False, + 'vip': [{ + 'name': 'my-vip', + 'node_roles': ['controller'], + }] + }}) + self.env.clusters[0].release.network_roles_metadata = net_roles + self.db.flush() + # VIPs are allocated on this call + config = self.env.neutron_networks_get(self.cluster.id).json_body + # Storage network has no GW by default + vip_config = config['vips']['my-vip']['ipaddr'] + self.assertEqual( + 1, + self.db.query(models.IPAddr).filter_by(ip_addr=vip_config).count() + ) + + resp = self.env.create_node_group() + self.assertEquals(resp.status_code, 201) + + # VIP address was deleted + self.assertEqual( + 0, + self.db.query(models.IPAddr).filter_by(ip_addr=vip_config).count() + ) + # Storage GW has this address now + resp = self.env.neutron_networks_get(self.cluster.id) + self.assertEquals(resp.status_code, 200) + config = resp.json_body + for net in config['networks']: + if net['name'] == consts.NETWORKS.storage: + self.assertEqual(vip_config, net['gateway']) + break + else: + self.fail('No storage network found') + # VIP is allocated to different address + self.assertNotEqual(vip_config, config['vips']['my-vip']['ipaddr']) + + @patch('nailgun.task.task.rpc.cast') + def test_ensure_gateways_present_cuts_ranges(self, _): + # setup particular networks without gateways + networks = { + 'public': {'cidr': '199.101.9.0/24', + 'ip_ranges': [['199.101.9.1', '199.101.9.1'], + ['199.101.9.5', '199.101.9.111']]}, + 'management': {'cidr': '199.101.1.0/24'}, + 'storage': {'cidr': '199.101.2.0/24'}, + } + config = self.env.neutron_networks_get(self.cluster.id).json_body + for net in config['networks']: + if net['name'] in networks: + for pkey, pval in six.iteritems(networks[net['name']]): + net[pkey] = pval + net['meta']['use_gateway'] = False + config['networking_parameters']['floating_ranges'] = [ + ['199.101.9.122', '199.101.9.233']] + resp = self.env.neutron_networks_put(self.cluster.id, config) + self.assertEquals(resp.status_code, 200) + + ranges_before = { + 'public': [['199.101.9.1', '199.101.9.1'], + ['199.101.9.5', '199.101.9.111']], + 'management': [['199.101.1.1', '199.101.1.254']], + 'storage': [['199.101.2.1', '199.101.2.254']], + } + config = self.env.neutron_networks_get(self.cluster.id).json_body + for net in config['networks']: + if net['name'] in networks: + self.assertEqual(ranges_before[net['name']], net['ip_ranges']) + + objects.Cluster.get_network_manager(self.cluster).\ + ensure_gateways_present_in_default_node_group(self.cluster) + + ranges_after = { + 'public': [['199.101.9.5', '199.101.9.111']], + 'management': [['199.101.1.2', '199.101.1.254']], + 'storage': [['199.101.2.2', '199.101.2.254']], + } + config = self.env.neutron_networks_get(self.cluster.id).json_body + for net in config['networks']: + if net['name'] in networks: + self.assertEqual(ranges_after[net['name']], net['ip_ranges']) + + def test_ensure_gateways_present_is_executed_once(self): + with patch.object( + objects.Cluster.get_network_manager(self.cluster), + 'ensure_gateways_present_in_default_node_group') as \ + ensure_mock: + + for n in range(2): + resp = self.env.create_node_group(name='group{0}'.format(n)) + self.assertEquals(resp.status_code, 201) + # one call is made when first custom node group is being added + # no more calls after that + self.assertEqual( + ensure_mock.call_count, 1, + 'Method was called {0} time(s) unexpectedly, ' + 'current node group: {1}'.format(ensure_mock.call_count, + resp.json_body['name'])) + + +class TestNodeGroupsVlan(TestNodeGroups): + + segmentation_type = consts.NEUTRON_SEGMENT_TYPES.vlan + + +class TestNodeGroupsTun(TestNodeGroups): + + segmentation_type = consts.NEUTRON_SEGMENT_TYPES.tun