fuel-qa/fuelweb_test/tests/test_net_templates_base.py

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': [7480],
'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]