c57e5fe33b
Closes-Bug: 1632332
Change-Id: I8ebac9d95de2a86bc9ddcbf2287b81ec9ff6fa06
(cherry picked from commit e26d83beef
)
500 lines
22 KiB
Python
500 lines
22 KiB
Python
# Copyright 2015 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.
|
|
|
|
import netaddr
|
|
|
|
from proboscis.asserts import assert_equal
|
|
from proboscis.asserts import assert_true
|
|
from proboscis.asserts import fail
|
|
|
|
from core.helpers.log_helpers import logwrap
|
|
|
|
from fuelweb_test import logger
|
|
from fuelweb_test.helpers.utils import get_ip_listen_stats
|
|
from fuelweb_test.tests.base_test_case import TestBasic
|
|
|
|
|
|
class TestNetworkTemplatesBase(TestBasic):
|
|
"""Base class to store all utility methods for network templates tests."""
|
|
|
|
@logwrap
|
|
def generate_networks_for_template(self, template, ip_nets,
|
|
ip_prefixlen):
|
|
"""Slice network to subnets for template.
|
|
|
|
Generate networks from network template and ip_nets descriptions
|
|
for node groups and value to slice that descriptions. ip_nets is a
|
|
dict with key named as nodegroup and strings values for with
|
|
description of network for that nodegroup in format '127.0.0.1/24'
|
|
to be sliced in pieces for networks. ip_prefixlen - the amount the
|
|
network prefix length should be sliced by. 24 will create networks
|
|
'127.0.0.1/24' from network '127.0.0.1/16'.
|
|
|
|
:param template: Yaml template with network assignments on interfaces.
|
|
:param ip_nets: Dict with network descriptions.
|
|
:param ip_prefixlen: Integer for slicing network prefix.
|
|
:return: Data to be used to assign networks to nodes
|
|
"""
|
|
networks_data = []
|
|
nodegroups = self.fuel_web.client.get_nodegroups()
|
|
for nodegroup, section in template['adv_net_template'].items():
|
|
assert_true(any(n['name'] == nodegroup for n in nodegroups),
|
|
'Network templates contains settings for Node Group '
|
|
'"{0}", which does not exist!'.format(nodegroup))
|
|
group_id = [n['id'] for n in nodegroups if
|
|
n['name'] == nodegroup][0]
|
|
ip_network = netaddr.IPNetwork(str(ip_nets[nodegroup]))
|
|
ip_subnets = list(ip_network.subnet(int(ip_prefixlen)))
|
|
for network in section['network_assignments']:
|
|
ip_subnet = ip_subnets.pop()
|
|
networks_data.append(
|
|
{
|
|
'name': network,
|
|
'cidr': str(ip_subnet),
|
|
'group_id': group_id,
|
|
'gateway': None,
|
|
'meta': {
|
|
"notation": "ip_ranges",
|
|
"render_type": None,
|
|
"map_priority": 0,
|
|
"configurable": True,
|
|
"unmovable": False,
|
|
"use_gateway": False,
|
|
"render_addr_mask": None,
|
|
'ip_range': [str(ip_subnet[1]), str(ip_subnet[-2])]
|
|
}
|
|
}
|
|
)
|
|
return networks_data
|
|
|
|
@logwrap
|
|
def map_group_by_iface_and_network(self, template):
|
|
""" Map groip id, iface name and network name
|
|
|
|
:param template: Yaml template with network assignments on interfaces.
|
|
:return: Data to be used for check of ip assignment
|
|
"""
|
|
mapped_data = {}
|
|
nodegroups = self.fuel_web.client.get_nodegroups()
|
|
for nodegroup, section in template['adv_net_template'].items():
|
|
networks = [(n, section['network_assignments'][n]['ep'])
|
|
for n in section['network_assignments']]
|
|
assert_true(any(n['name'] == nodegroup for n in nodegroups),
|
|
'Network templates contains settings for Node Group '
|
|
'"{0}", which does not exist!'.format(nodegroup))
|
|
group_id = [n['id'] for n in nodegroups if
|
|
n['name'] == nodegroup][0]
|
|
mapped_data[group_id] = dict(networks)
|
|
return mapped_data
|
|
|
|
@staticmethod
|
|
@logwrap
|
|
def get_template_ep_for_role(template, role, nodegroup='default',
|
|
skip_net_roles=None):
|
|
if skip_net_roles is None:
|
|
skip_net_roles = set()
|
|
tmpl = template['adv_net_template'][nodegroup]
|
|
endpoints = set()
|
|
networks = set()
|
|
network_types = tmpl['templates_for_node_role'][role]
|
|
for network_type in network_types:
|
|
endpoints.update(tmpl['network_scheme'][network_type]['endpoints'])
|
|
for scheme_type in tmpl['network_scheme']:
|
|
for net_role in tmpl['network_scheme'][scheme_type]['roles']:
|
|
if net_role in skip_net_roles:
|
|
endpoints.discard(
|
|
tmpl['network_scheme'][scheme_type]['roles'][net_role])
|
|
for net in tmpl['network_assignments']:
|
|
if tmpl['network_assignments'][net]['ep'] in endpoints:
|
|
networks.add(net)
|
|
return networks
|
|
|
|
@staticmethod
|
|
@logwrap
|
|
def get_template_netroles_for_role(template, role, nodegroup='default'):
|
|
tmpl = template['adv_net_template'][nodegroup]
|
|
netroles = dict()
|
|
network_types = tmpl['templates_for_node_role'][role]
|
|
for network_type in network_types:
|
|
netroles.update(tmpl['network_scheme'][network_type]['roles'])
|
|
return netroles
|
|
|
|
@logwrap
|
|
def create_custom_networks(self, networks, existing_networks):
|
|
for custom_net in networks:
|
|
if not any([custom_net['name'] == n['name'] and
|
|
# ID of 'fuelweb_admin' default network group is None
|
|
custom_net['group_id'] == (n['group_id'] or 1)
|
|
for n in existing_networks]):
|
|
self.fuel_web.client.add_network_group(custom_net)
|
|
else:
|
|
# Copying settings from existing network
|
|
net = [n for n in existing_networks if
|
|
custom_net['name'] == n['name'] and
|
|
custom_net['group_id'] == (n['group_id'] or 1)][0]
|
|
custom_net['cidr'] = net['cidr']
|
|
custom_net['meta'] = net['meta']
|
|
custom_net['gateway'] = net['gateway']
|
|
return networks
|
|
|
|
@staticmethod
|
|
@logwrap
|
|
def get_interface_ips(remote, iface_name):
|
|
cmd = ("set -o pipefail; "
|
|
"ip -o -4 address show dev {0} | sed -rn "
|
|
"'s/^.*\sinet\s+([0-9\.]+\/[0-9]{{1,2}})\s.*$/\\1/p'").format(
|
|
iface_name)
|
|
result = remote.execute(cmd)
|
|
logger.debug("Checking interface IP result: {0}".format(result))
|
|
assert_equal(result['exit_code'], 0,
|
|
"Device {0} not found on remote node!".format(iface_name))
|
|
return [line.strip() for line in result['stdout']]
|
|
|
|
@logwrap
|
|
def check_interface_ip_exists(self, remote, iface_name, cidr):
|
|
raw_addresses = self.get_interface_ips(remote, iface_name)
|
|
raw_ips = [raw_addr.split('/')[0] for raw_addr in raw_addresses]
|
|
try:
|
|
ips = [netaddr.IPAddress(str(raw_ip)) for raw_ip in raw_ips]
|
|
except ValueError:
|
|
fail('Device {0} on remote node does not have a valid '
|
|
'IPv4 address assigned!'.format(iface_name))
|
|
return
|
|
actual_networks = [netaddr.IPNetwork(str(raw_addr)) for
|
|
raw_addr in raw_addresses]
|
|
network = netaddr.IPNetwork(str(cidr))
|
|
assert_true(network in actual_networks,
|
|
'Network(s) on {0} device differs than {1}: {2}'.format(
|
|
iface_name, cidr, raw_addresses))
|
|
assert_true(any(ip in network for ip in ips),
|
|
'IP address on {0} device is not from {1} network!'.format(
|
|
iface_name, cidr))
|
|
|
|
@logwrap
|
|
def check_ipconfig_for_template(self, cluster_id, network_template,
|
|
networks):
|
|
logger.info("Checking that IP addresses configuration on nodes "
|
|
"corresponds to used networking template...")
|
|
# Network for Neutron is configured in namespaces (l3/dhcp agents)
|
|
# and a bridge for it doesn't have IP, so skipping it for now
|
|
skip_roles = {'neutron/private'}
|
|
mapped_data = self.map_group_by_iface_and_network(network_template)
|
|
for node in self.fuel_web.client.list_cluster_nodes(cluster_id):
|
|
node_networks = set()
|
|
node_group_name = [ng['name'] for ng in
|
|
self.fuel_web.client.get_nodegroups()
|
|
if ng['id'] == node['group_id']][0]
|
|
for role in node['roles']:
|
|
node_networks.update(
|
|
self.get_template_ep_for_role(template=network_template,
|
|
role=role,
|
|
nodegroup=node_group_name,
|
|
skip_net_roles=skip_roles))
|
|
with self.env.d_env.get_ssh_to_remote(node['ip']) as remote:
|
|
for network in networks:
|
|
if network['name'] not in node_networks or \
|
|
network['group_id'] != node['group_id']:
|
|
continue
|
|
logger.debug(
|
|
'Checking interface "{0}" for IP network '
|
|
'"{1}" on "{2}"'.format(
|
|
mapped_data[node['group_id']][network['name']],
|
|
network['cidr'],
|
|
node['hostname']))
|
|
self.check_interface_ip_exists(
|
|
remote,
|
|
mapped_data[node['group_id']][network['name']],
|
|
network['cidr'])
|
|
|
|
@staticmethod
|
|
@logwrap
|
|
def get_port_listen_ips(listen_stats, port):
|
|
ips = set()
|
|
for socket in listen_stats:
|
|
hexip, hexport = socket.split(':')
|
|
if int(port) == int(hexport, 16):
|
|
ips.add('.'.join([str(int(hexip[n:n + 2], 16))
|
|
for n in range(0, len(hexip), 2)][::-1]))
|
|
return ips
|
|
|
|
@logwrap
|
|
def check_services_networks(self, cluster_id, net_template):
|
|
logger.info("Checking that OpenStack services on nodes are listening "
|
|
"on IP networks according to used networking template...")
|
|
services = [
|
|
{
|
|
'name': 'keystone_api',
|
|
'network_roles': ['keystone/api'],
|
|
'tcp_ports': [5000, 35357],
|
|
'udp_ports': [],
|
|
# check is disabled because access to API is restricted
|
|
# using firewall (see LP#1489057,
|
|
# https://review.openstack.org/#/c/218853/)
|
|
'enabled': False
|
|
},
|
|
{
|
|
'name': 'nova-api',
|
|
'network_roles': ['nova/api'],
|
|
'tcp_ports': [8773, 8774],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'neutron-api',
|
|
'network_roles': ['neutron/api'],
|
|
'tcp_ports': [9696],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'swift-api',
|
|
'network_roles': ['swift/api'],
|
|
'tcp_ports': [8080],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'swift-replication',
|
|
'network_roles': ['swift/replication'],
|
|
'tcp_ports': [6000, 6001, 6002],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'sahara-api',
|
|
'network_roles': ['sahara/api'],
|
|
'tcp_ports': [8386],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'ceilometer-api',
|
|
'network_roles': ['ceilometer/api'],
|
|
'tcp_ports': [8777],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'cinder-api',
|
|
'network_roles': ['cinder/api'],
|
|
'tcp_ports': [8776],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'glance-api',
|
|
'network_roles': ['glance/api'],
|
|
'tcp_ports': [5509],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'heat-api',
|
|
'network_roles': ['heat/api'],
|
|
'tcp_ports': [8000, 8003, 8004],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'murano-api',
|
|
'network_roles': ['murano/api'],
|
|
'tcp_ports': [8082],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'ceph',
|
|
'network_roles': ['ceph/replication', 'ceph/public'],
|
|
'tcp_ports': [6804, 6805, 6806, 6807],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'ceph-radosgw',
|
|
'network_roles': ['ceph/radosgw'],
|
|
'tcp_ports': [6780],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'mongo-db',
|
|
'network_roles': ['mongo/db'],
|
|
'tcp_ports': [27017],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'mgmt-messaging',
|
|
'network_roles': ['mgmt/messaging'],
|
|
'tcp_ports': [5673],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'mgmt-corosync',
|
|
'network_roles': ['mgmt/corosync'],
|
|
'tcp_ports': [],
|
|
'udp_ports': [5405],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'mgmt-memcache',
|
|
'network_roles': ['mgmt/memcache'],
|
|
'tcp_ports': [11211],
|
|
'udp_ports': [11211],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'mgmt-database',
|
|
'network_roles': ['mgmt/database'],
|
|
'tcp_ports': [3307, 4567],
|
|
'udp_ports': [],
|
|
'enabled': True
|
|
},
|
|
{
|
|
'name': 'cinder-iscsi',
|
|
'network_roles': ['cinder/iscsi'],
|
|
'tcp_ports': [3260],
|
|
'udp_ports': [],
|
|
# ISCSI daemon is started automatically because cinder-volume
|
|
# package installs it by dependencies (LP#1491518)
|
|
'enabled': False
|
|
},
|
|
]
|
|
|
|
check_passed = True
|
|
|
|
for node in self.fuel_web.client.list_cluster_nodes(cluster_id):
|
|
node_netroles = dict()
|
|
node_group_name = [ng['name'] for ng in
|
|
self.fuel_web.client.get_nodegroups()
|
|
if ng['id'] == node['group_id']][0]
|
|
for role in node['roles']:
|
|
node_netroles.update(self.get_template_netroles_for_role(
|
|
template=net_template,
|
|
role=role,
|
|
nodegroup=node_group_name))
|
|
|
|
with self.env.d_env.get_ssh_to_remote(node['ip']) as remote:
|
|
tcp_listen_stats = get_ip_listen_stats(remote, 'tcp')
|
|
udp_listen_stats = get_ip_listen_stats(remote, 'udp')
|
|
for service in services:
|
|
if any(net_role not in node_netroles.keys()
|
|
for net_role in service['network_roles']) \
|
|
or not service['enabled']:
|
|
continue
|
|
ips = set()
|
|
for service_net_role in service['network_roles']:
|
|
iface_name = node_netroles[service_net_role]
|
|
ips.update([cidr.split('/')[0] for cidr in
|
|
self.get_interface_ips(remote,
|
|
iface_name)])
|
|
|
|
for port in service['tcp_ports']:
|
|
listen_ips = self.get_port_listen_ips(tcp_listen_stats,
|
|
port)
|
|
if not listen_ips:
|
|
logger.debug('Service "{0}" is not found on '
|
|
'"{1}".'.format(service['name'],
|
|
node['hostname']))
|
|
continue
|
|
if any(lip not in ips for lip in listen_ips):
|
|
check_passed = False
|
|
logger.error('Service "{0}" (port {4}/tcp) is '
|
|
'listening on wrong IP address(es) '
|
|
'on "{1}": expected "{2}", got '
|
|
'"{3}"!'.format(service['name'],
|
|
node['hostname'],
|
|
ips,
|
|
listen_ips,
|
|
port))
|
|
for port in service['udp_ports']:
|
|
listen_ips = self.get_port_listen_ips(udp_listen_stats,
|
|
port)
|
|
if not listen_ips:
|
|
logger.debug('Service "{0}" is not found on '
|
|
'"{1}".'.format(service['name'],
|
|
node['hostname']))
|
|
continue
|
|
if any(lip not in ips for lip in listen_ips):
|
|
check_passed = False
|
|
logger.error('Service "{0}" (port {4}/udp) is '
|
|
'listening on wrong IP address(es) '
|
|
'on "{1}": expected "{2}", got '
|
|
'"{3}"!'.format(service['name'],
|
|
node['hostname'],
|
|
ips,
|
|
listen_ips,
|
|
port))
|
|
assert_true(check_passed,
|
|
'Some services are listening on wrong IPs! '
|
|
'Please check logs for details!')
|
|
|
|
@staticmethod
|
|
def get_modified_ranges(net_dict, net_name, group_id):
|
|
for net in net_dict['networks']:
|
|
if net_name in net['name'] and net['group_id'] == group_id:
|
|
cidr = net['cidr']
|
|
sliced_list = list(netaddr.IPNetwork(str(cidr)))[5:-5]
|
|
return [str(sliced_list[0]), str(sliced_list[-1])]
|
|
|
|
@staticmethod
|
|
def change_default_admin_range(networks, number_excluded_ips):
|
|
"""Change IP range for admin network by excluding N of first addresses
|
|
from default range
|
|
:param networks: list, environment networks configuration
|
|
:param number_excluded_ips: int, number of IPs to remove from range
|
|
"""
|
|
default_admin_network = [n for n in networks
|
|
if (n['name'] == "fuelweb_admin" and
|
|
n['group_id'] is None)]
|
|
assert_true(len(default_admin_network) == 1,
|
|
"Default 'admin/pxe' network not found "
|
|
"in cluster network configuration!")
|
|
default_admin_range = [netaddr.IPAddress(str(ip)) for ip
|
|
in default_admin_network[0]["ip_ranges"][0]]
|
|
new_admin_range = [default_admin_range[0] + number_excluded_ips,
|
|
default_admin_range[1]]
|
|
default_admin_network[0]["ip_ranges"][0] = [str(ip)
|
|
for ip in new_admin_range]
|
|
return default_admin_network[0]["ip_ranges"][0]
|
|
|
|
@staticmethod
|
|
def is_ip_in_range(ip_addr, ip_range_start, ip_range_end):
|
|
return netaddr.IPAddress(str(ip_addr)) in netaddr.iter_iprange(
|
|
str(ip_range_start), str(ip_range_end))
|
|
|
|
@staticmethod
|
|
def is_update_dnsmasq_running(tasks):
|
|
for task in tasks:
|
|
if task['name'] == "update_dnsmasq" and \
|
|
task["status"] == "running":
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
def update_network_ranges(net_data, update_data):
|
|
for net in net_data['networks']:
|
|
for group in update_data:
|
|
for net_name in update_data[group]:
|
|
if net_name in net['name'] and net['group_id'] == group:
|
|
net['ip_ranges'] = update_data[group][net_name]
|
|
net['meta']['notation'] = 'ip_ranges'
|
|
return net_data
|
|
|
|
@staticmethod
|
|
def get_ranges(net_data, net_name, group_id):
|
|
return [net['ip_ranges'] for net in net_data['networks'] if
|
|
net_name in net['name'] and group_id == net['group_id']][0]
|