Add extra dhcp options tests
Tests moved from downstream plugin with minimal adjustments. These tests require nmcli inside the server so advanced image changed to rocky-9 cloud image. Other alternative, e.g. centos-9 does not have a default user and requires additional customization for setting the user. Additionally, moved utils.py and constants.py. From config.py and base.py only relevant subset of code moved. Change-Id: I6b538c13a15e94d072091b3218b2cd48ff20a70c
This commit is contained in:
parent
b53e623b63
commit
fa2d7457a8
3
devstack/plugin.sh
Normal file
3
devstack/plugin.sh
Normal file
@ -0,0 +1,3 @@
|
||||
if [[ "$1" == "stack" ]] && [[ "$2" == "install" ]]; then
|
||||
echo "tempest ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/99_tempest
|
||||
fi
|
3
tox.ini
3
tox.ini
@ -33,7 +33,8 @@ commands = {posargs}
|
||||
# H904: Delay string interpolations at logging calls
|
||||
enable-extensions = H106,H203,H204,H205,H904
|
||||
# H405: multi line docstring summary not separated with an empty line
|
||||
ignore = H405
|
||||
# W504: line break after binary operator
|
||||
ignore = H405,W504
|
||||
show-source = true
|
||||
exclude = ./.*,build,dist,doc,*egg*,releasenotes
|
||||
import-order-style = pep8
|
||||
|
42
whitebox_neutron_tempest_plugin/common/constants.py
Normal file
42
whitebox_neutron_tempest_plugin/common/constants.py
Normal file
@ -0,0 +1,42 @@
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
GLOBAL_IP = '1.1.1.1'
|
||||
NCAT_PORT = 65000
|
||||
NCAT_TIMEOUT = 30
|
||||
IP_HEADER_LENGTH = 20
|
||||
TCP_HEADER_LENGTH = 20
|
||||
UDP_HEADER_LENGTH = 8
|
||||
ETHERNET_HEADER_LENGTH = 18
|
||||
STATE_UP = 'up'
|
||||
STATE_DOWN = 'down'
|
||||
ACTION_STOP = 'stop'
|
||||
ACTION_START = 'start'
|
||||
DHCP_OPTIONS_NMCLI_TO_NEUTRON = {
|
||||
'dhcp_lease_time': 'lease-time',
|
||||
'domain_name': 'domain-name',
|
||||
'domain_name_servers': 'dns-server',
|
||||
'interface_mtu': 'mtu',
|
||||
'dhcp6_domain_search': 'domain-search',
|
||||
'dhcp6_name_servers': 'dns-server'}
|
||||
DHCP_OPTIONS_NUMBER_TO_NAME = {
|
||||
'26': 'mtu'}
|
||||
DHCP_OPTIONS_NEUTRON_TO_OVN = {
|
||||
'domain-name': 'domain_name',
|
||||
'mtu': 'mtu',
|
||||
'dns-server': 'dns_server',
|
||||
'lease-time': 'lease_time',
|
||||
'domain-search': 'domain_search',
|
||||
'26': 'mtu'}
|
214
whitebox_neutron_tempest_plugin/common/utils.py
Normal file
214
whitebox_neutron_tempest_plugin/common/utils.py
Normal file
@ -0,0 +1,214 @@
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from neutron_tempest_plugin.common import shell
|
||||
from neutron_tempest_plugin.common import utils as common_utils
|
||||
from oslo_log import log
|
||||
from tempest import config
|
||||
from tempest.lib import exceptions
|
||||
|
||||
from whitebox_neutron_tempest_plugin.common import constants
|
||||
|
||||
CONF = config.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def create_payload_file(ssh_client, size):
|
||||
ssh_client.exec_command(
|
||||
"head -c {0} /dev/zero > {0}".format(size))
|
||||
|
||||
|
||||
def get_temp_file(ssh_client):
|
||||
output_file = ssh_client.exec_command(
|
||||
'mktemp').rstrip()
|
||||
return output_file
|
||||
|
||||
|
||||
def cat_remote_file(ssh_client, path):
|
||||
return ssh_client.exec_command(
|
||||
'cat {}'.format(path)).rstrip()
|
||||
|
||||
|
||||
def get_default_interface(ssh_client):
|
||||
return ssh_client.exec_command(
|
||||
"PATH=$PATH:/usr/sbin ip route get default %s | head -1 | "
|
||||
"cut -d ' ' -f 5" % constants.GLOBAL_IP).rstrip()
|
||||
|
||||
|
||||
def get_route_interface(ssh_client, dst_ip):
|
||||
output = ssh_client.exec_command(
|
||||
"PATH=$PATH:/usr/sbin ip route get default %s | head -1" % dst_ip)
|
||||
if output:
|
||||
for line in output.splitlines():
|
||||
fields = line.strip().split()
|
||||
device_index = fields.index('dev') + 1
|
||||
return fields[device_index]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def make_sure_local_port_is_open(protocol, port):
|
||||
shell.execute_local_command(
|
||||
"sudo iptables-save | "
|
||||
r"grep 'INPUT.*{protocol}.*\-\-dport {port} \-j ACCEPT' "
|
||||
"&& true || "
|
||||
"sudo iptables -I INPUT 1 -p {protocol} --dport {port} -j ACCEPT"
|
||||
"".format(protocol=protocol, port=port))
|
||||
|
||||
|
||||
# Unlike ncat server function from the upstream plugin this ncat server
|
||||
# turns itself off automatically after timeout
|
||||
def run_ncat_server(ssh_client, udp):
|
||||
output_file = get_temp_file(ssh_client)
|
||||
cmd = "sudo timeout {0} nc -l {1} -p {2} > {3}".format(
|
||||
constants.NCAT_TIMEOUT, udp, constants.NCAT_PORT, output_file)
|
||||
LOG.debug("Starting nc server: '%s'", cmd)
|
||||
ssh_client.open_session().exec_command(cmd)
|
||||
return output_file
|
||||
|
||||
|
||||
# Unlike ncat client function from the upstream plugin this ncat client
|
||||
# is able to run from any host, not only locally
|
||||
def run_ncat_client(ssh_client, host, udp, payload_size):
|
||||
cmd = "nc -w 1 {0} {1} {2} < {3}".format(
|
||||
host, udp, constants.NCAT_PORT, payload_size)
|
||||
LOG.debug("Starting nc client: '%s'", cmd)
|
||||
ssh_client.exec_command(cmd)
|
||||
|
||||
|
||||
def flush_routing_cache(ssh_client):
|
||||
ssh_client.exec_command("sudo ip route flush cache")
|
||||
|
||||
|
||||
def kill_iperf_process(ssh_client):
|
||||
cmd = "PATH=$PATH:/usr/sbin pkill iperf3"
|
||||
try:
|
||||
ssh_client.exec_command(cmd)
|
||||
except exceptions.SSHExecCommandFailed:
|
||||
pass
|
||||
|
||||
|
||||
def configure_interface_up(client, port, interface=None, path=None):
|
||||
"""configures down interface with ip and activates it
|
||||
|
||||
Parameters:
|
||||
client (ssh.Client):ssh client which has interface to configure.
|
||||
port (port):port object of interface.
|
||||
interface (str):optional interface name on vm.
|
||||
path (str):optional shell PATH variable.
|
||||
"""
|
||||
shell_path = path or "PATH=$PATH:/sbin"
|
||||
test_interface = interface or client.exec_command(
|
||||
"{};ip addr | grep {} -B 1 | head -1 | "
|
||||
r"cut -d ':' -f 2 | sed 's/\ //g'".format(
|
||||
shell_path, port['mac_address'])).rstrip()
|
||||
|
||||
if CONF.neutron_plugin_options.default_image_is_advanced:
|
||||
cmd = ("ip addr show {interface} | grep {ip} || "
|
||||
"sudo dhclient {interface}").format(
|
||||
ip=port['fixed_ips'][0]['ip_address'],
|
||||
interface=test_interface)
|
||||
else:
|
||||
cmd = ("cat /sys/class/net/{interface}/operstate | "
|
||||
"grep -q -v down && true || "
|
||||
"({path}; sudo ip link set {interface} up && "
|
||||
"sudo ip addr add {ip}/24 dev {interface})").format(
|
||||
path=shell_path,
|
||||
ip=port['fixed_ips'][0]['ip_address'],
|
||||
interface=test_interface)
|
||||
|
||||
common_utils.wait_until_true(
|
||||
lambda: execute_command_safely(client, cmd), timeout=30, sleep=5)
|
||||
|
||||
|
||||
def parse_dhcp_options_from_nmcli(
|
||||
ssh_client, ip_version,
|
||||
timeout=20.0, interval=5.0, expected_empty=False, vlan=None):
|
||||
# first of all, test ssh connection is available - the time it takes until
|
||||
# ssh connection can be established is not cosidered for the nmcli timeout
|
||||
ssh_client.test_connection_auth()
|
||||
# Add grep -v to exclude loopback interface because
|
||||
# Managing the lookback interface using NetworkManager is included in
|
||||
# RHEL9.2 image. Previous version is not included.
|
||||
cmd_find_connection = 'nmcli -g NAME con show --active | grep -v "^lo"'
|
||||
if vlan is not None:
|
||||
cmd_find_connection += ' | grep {}'.format(vlan)
|
||||
cmd_show_dhcp = ('sudo nmcli -f DHCP{} con show '
|
||||
'"$({})"').format(ip_version, cmd_find_connection)
|
||||
|
||||
start_time = time.time()
|
||||
while True:
|
||||
try:
|
||||
output = ssh_client.exec_command(cmd_show_dhcp)
|
||||
except exceptions.SSHExecCommandFailed:
|
||||
LOG.warning('Failed to run nmcli on VM - retrying...')
|
||||
else:
|
||||
if not output and not expected_empty:
|
||||
LOG.warning('nmcli result on VM is empty - retrying...')
|
||||
else:
|
||||
break
|
||||
if time.time() - start_time > timeout:
|
||||
message = ('Failed to run nmcli on VM after {} '
|
||||
'seconds'.format(timeout))
|
||||
raise exceptions.TimeoutException(message)
|
||||
time.sleep(interval)
|
||||
|
||||
if not output:
|
||||
return None
|
||||
obtained_dhcp_opts = {}
|
||||
for line in output.splitlines():
|
||||
newline = re.sub(r'^DHCP{}.OPTION\[[0-9]+\]:\s+'.format(ip_version),
|
||||
'', line.strip())
|
||||
option = newline.split('=')[0].strip()
|
||||
value = newline.split('=')[1].strip()
|
||||
if option in constants.DHCP_OPTIONS_NMCLI_TO_NEUTRON:
|
||||
option = constants.DHCP_OPTIONS_NMCLI_TO_NEUTRON[option]
|
||||
obtained_dhcp_opts[option] = value
|
||||
return obtained_dhcp_opts
|
||||
|
||||
|
||||
def execute_command_safely(ssh_client, command):
|
||||
try:
|
||||
output = ssh_client.exec_command(command)
|
||||
except exceptions.SSHExecCommandFailed as err:
|
||||
LOG.warning('command failed: %s', command)
|
||||
LOG.exception(err)
|
||||
return False
|
||||
LOG.debug('command executed successfully: %s\n'
|
||||
'command output:\n%s',
|
||||
command, output)
|
||||
return True
|
||||
|
||||
|
||||
def host_responds_to_ping(ip, count=3):
|
||||
cmd = "ping -c{} {}".format(count, ip)
|
||||
try:
|
||||
subprocess.check_output(['bash', '-c', cmd])
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run_local_cmd(cmd, timeout=10):
|
||||
command = "timeout " + str(timeout) + " " + cmd
|
||||
LOG.debug("Running local command '{}'".format(command))
|
||||
output, errors = subprocess.Popen(
|
||||
command, shell=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE).communicate()
|
||||
return output, errors
|
@ -21,4 +21,21 @@ whitebox_neutron_plugin_options = cfg.OptGroup(
|
||||
title="Whitebox neutron tempest plugin config options"
|
||||
)
|
||||
|
||||
WhiteboxNeutronPluginOptions = []
|
||||
WhiteboxNeutronPluginOptions = [
|
||||
cfg.StrOpt('openstack_type',
|
||||
default='devstack',
|
||||
help='Type of openstack deployment, '
|
||||
'e.g. devstack, tripeo, podified'),
|
||||
cfg.StrOpt('pki_private_key',
|
||||
default='/etc/pki/tls/private/ovn_controller.key',
|
||||
help='File with private key. Need for TLS-everywhere '
|
||||
'environments.'),
|
||||
cfg.StrOpt('pki_certificate',
|
||||
default='/etc/pki/tls/certs/ovn_controller.crt',
|
||||
help='File with certificate for private key. Need for '
|
||||
'TLS-everywhere environments.'),
|
||||
cfg.StrOpt('pki_ca_cert',
|
||||
default='/etc/ipa/ca.crt',
|
||||
help='File with peer CA certificate. Need for TLS-everywhere '
|
||||
'environments.')
|
||||
]
|
||||
|
187
whitebox_neutron_tempest_plugin/tests/scenario/base.py
Normal file
187
whitebox_neutron_tempest_plugin/tests/scenario/base.py
Normal file
@ -0,0 +1,187 @@
|
||||
# Copyright 2019 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 re
|
||||
|
||||
import netaddr
|
||||
from neutron_tempest_plugin.scenario import base
|
||||
from neutron_tempest_plugin.common import utils as common_utils
|
||||
from oslo_log import log
|
||||
from tempest import config
|
||||
|
||||
from whitebox_neutron_tempest_plugin.common import utils as local_utils
|
||||
|
||||
CONF = config.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
WB_CONF = config.CONF.whitebox_neutron_plugin_options
|
||||
|
||||
|
||||
class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase):
|
||||
credentials = ['primary', 'admin']
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
super(BaseTempestWhiteboxTestCase, cls).resource_setup()
|
||||
uri = CONF.identity.uri
|
||||
cls.is_ipv6 = True if netaddr.valid_ipv6(
|
||||
uri[uri.find("[") + 1:uri.find("]")]) else False
|
||||
cls.image_ref = CONF.compute.image_ref
|
||||
cls.flavor_ref = CONF.compute.flavor_ref
|
||||
cls.username = CONF.validation.image_ssh_user
|
||||
|
||||
@classmethod
|
||||
def run_on_master_controller(cls, cmd):
|
||||
if WB_CONF.openstack_type == 'devstack':
|
||||
output, errors = local_utils.run_local_cmd(cmd)
|
||||
LOG.debug("Stderr: {}".format(errors.decode()))
|
||||
output = output.decode()
|
||||
LOG.debug("Output: {}".format(output))
|
||||
return output.strip()
|
||||
|
||||
|
||||
class BaseTempestTestCaseOvn(BaseTempestWhiteboxTestCase):
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
super(BaseTempestTestCaseOvn, cls).resource_setup()
|
||||
agents = cls.os_admin.network.AgentsClient().list_agents()['agents']
|
||||
ovn_agents = [agent for agent in agents if 'ovn' in agent['binary']]
|
||||
if not ovn_agents:
|
||||
raise cls.skipException(
|
||||
"OVN agents not found. This test is supported only on "
|
||||
"openstack environments with OVN support.")
|
||||
|
||||
cls.nbctl, cls.sbctl = cls._get_ovn_dbs()
|
||||
cls.nbmonitorcmd, cls.sbmonitorcmd = cls._get_ovn_db_monitor_cmds()
|
||||
|
||||
@classmethod
|
||||
def _get_ovn_db_monitor_cmds(cls):
|
||||
regex = r'--db=(.*)$'
|
||||
# this regex search will return the connection string (tcp:IP:port or
|
||||
# ssl:IP:port) and in case of TLS, will also include the TLS options
|
||||
nb_monitor_connection_opts = re.search(regex, cls.nbctl).group(1)
|
||||
sb_monitor_connection_opts = re.search(regex, cls.sbctl).group(1)
|
||||
monitorcmdprefix = 'sudo timeout 300 ovsdb-client monitor -f json '
|
||||
return (monitorcmdprefix + nb_monitor_connection_opts,
|
||||
monitorcmdprefix + sb_monitor_connection_opts)
|
||||
|
||||
@classmethod
|
||||
def _get_ovn_dbs(cls):
|
||||
ssl_params = ''
|
||||
if WB_CONF.openstack_type == 'tripleo':
|
||||
cmd = ("sudo ovs-vsctl get open . external_ids:ovn-remote | "
|
||||
"sed -e 's/\"//g'")
|
||||
sbdb = cls.run_on_master_controller(cmd)
|
||||
if 'ssl' in sbdb:
|
||||
ssl_params = '-p {} -c {} -C {} '.format(
|
||||
WB_CONF.pki_private_key,
|
||||
WB_CONF.pki_certificate,
|
||||
WB_CONF.pki_ca_cert)
|
||||
nbdb = sbdb.replace('6642', '6641')
|
||||
cmd = 'ovn-{}ctl --db={} %s' % (ssl_params)
|
||||
cmd = 'sudo %s exec ovn_controller %s' % (cls.container_app, cmd)
|
||||
if WB_CONF.openstack_type == 'devstack':
|
||||
sbdb = "unix:/usr/local/var/run/ovn/ovnsb_db.sock"
|
||||
nbdb = sbdb.replace('sb', 'nb')
|
||||
cmd = ("sudo ovn-{}ctl --db={}")
|
||||
return [cmd.format('nb', nbdb), cmd.format('sb', sbdb)]
|
||||
|
||||
def get_router_gateway_chassis(self, router_port_id):
|
||||
cmd = "{} get port_binding cr-lrp-{} chassis".format(
|
||||
self.sbctl, router_port_id)
|
||||
LOG.debug("Waiting until port is bound to chassis")
|
||||
self.chassis_id = None
|
||||
|
||||
def _port_binding_exist():
|
||||
self.chassis_id = self.run_on_master_controller(cmd)
|
||||
LOG.debug("chassis_id = '{}'".format(self.chassis_id))
|
||||
if self.chassis_id != '[]':
|
||||
return True
|
||||
return False
|
||||
|
||||
try:
|
||||
common_utils.wait_until_true(lambda: _port_binding_exist(),
|
||||
timeout=30, sleep=5)
|
||||
except common_utils.WaitTimeout:
|
||||
self.fail("Port is not bound to chassis")
|
||||
cmd = "{} get chassis {} hostname".format(self.sbctl, self.chassis_id)
|
||||
LOG.debug("Running '{}' on the master node".format(cmd))
|
||||
res = self.run_on_master_controller(cmd)
|
||||
return res.replace('"', '').split('.')[0]
|
||||
|
||||
def get_router_gateway_chassis_list(self, router_port_id):
|
||||
cmd = (self.nbctl + " lrp-get-gateway-chassis lrp-" + router_port_id)
|
||||
data = self.run_on_master_controller(cmd)
|
||||
return [re.sub(r'.*_(.*?)\s.*', r'\1', s) for s in data.splitlines()]
|
||||
|
||||
def get_router_gateway_chassis_by_id(self, chassis_id):
|
||||
res = self.run_on_master_controller(
|
||||
self.sbctl + " get chassis " + chassis_id + " hostname").rstrip()
|
||||
return res.replace('"', '').split('.')[0]
|
||||
|
||||
def get_router_port_gateway_mtu(self, router_port_id):
|
||||
cmd = (self.nbctl + " get logical_router_port lrp-" + router_port_id +
|
||||
" options:gateway_mtu")
|
||||
return int(
|
||||
self.run_on_master_controller(cmd).rstrip().strip('"'))
|
||||
|
||||
def get_item_uuid(self, db, item, search_string):
|
||||
ovn_db = self.sbctl if db == 'sb' else self.nbctl
|
||||
cmd = (ovn_db + " find " + item + " " + search_string +
|
||||
" | grep _uuid | awk '{print $3}'")
|
||||
return self.run_on_master_controller(cmd)
|
||||
|
||||
def get_datapath_tunnel_key(self, search_string):
|
||||
cmd = (self.sbctl + " find datapath_binding " + search_string +
|
||||
" | grep tunnel_key | awk '{print $3}'")
|
||||
return self.run_on_master_controller(cmd)
|
||||
|
||||
def get_logical_switch(self, port):
|
||||
"""Returns logical switch name that port is connected to
|
||||
|
||||
Fuction gets the logical switch name without its ID from the
|
||||
`ovn-nbctl lsp-get-ls <PORT_NAME>` command
|
||||
"""
|
||||
cmd = '{cmd} lsp-get-ls {port}'.format(cmd=self.nbctl, port=port)
|
||||
output = self.run_on_master_controller(cmd)
|
||||
ls_name = re.search('neutron-[^)]*', output)
|
||||
if ls_name:
|
||||
return ls_name.group()
|
||||
else:
|
||||
return ''
|
||||
|
||||
def get_physical_net(self, port):
|
||||
"""Returns physical network name that port has configured with
|
||||
|
||||
Physical network name is saved as option in the logical switch port
|
||||
record in OVN north database. It can be queried with
|
||||
`ovn-nbctl lsp-get-options <PORT_NAME>` command but this output may
|
||||
contain more than one option so it is better to get the value with
|
||||
`ovn-nbctl get Logical_Switch_Port <PORT_NAME> options:network_name`
|
||||
command
|
||||
"""
|
||||
cmd = '{cmd} get Logical_Switch_Port {port} '\
|
||||
'options:network_name'.format(cmd=self.nbctl, port=port)
|
||||
return self.run_on_master_controller(cmd)
|
||||
|
||||
def verify_that_segment_deleted(self, segment_id):
|
||||
"""Checks that the segment id is not in the OVN database
|
||||
|
||||
There shouldn't be 'provnet-<SEGEMTN_ID>' port in the OVN database
|
||||
after the segment has been deleted
|
||||
"""
|
||||
cmd = '{cmd} find Logical_Switch_Port '\
|
||||
'name=provnet-{sid}'.format(cmd=self.nbctl, sid=segment_id)
|
||||
output = self.run_on_master_controller(cmd)
|
||||
self.assertEqual(output, '')
|
@ -0,0 +1,369 @@
|
||||
# Copyright 2021 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 re
|
||||
|
||||
from neutron_lib import constants as lib_constants
|
||||
from neutron_tempest_plugin.common import ssh
|
||||
from neutron_tempest_plugin import config
|
||||
from neutron_tempest_plugin.scenario import base
|
||||
from neutron_tempest_plugin.scenario import constants as neutron_constants
|
||||
from oslo_log import log
|
||||
from tempest.common import waiters
|
||||
from tempest.lib.common.utils import data_utils
|
||||
from tempest.lib import decorators
|
||||
|
||||
from whitebox_neutron_tempest_plugin.common import constants
|
||||
from whitebox_neutron_tempest_plugin.common import utils
|
||||
from whitebox_neutron_tempest_plugin.tests.scenario import base as ds_base
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
IPV4 = lib_constants.IP_VERSION_4
|
||||
IPV6 = lib_constants.IP_VERSION_6
|
||||
|
||||
|
||||
class ExtraDhcpOptionsTest(base.BaseTempestTestCase):
|
||||
credentials = ['primary', 'admin']
|
||||
required_extensions = ['extra_dhcp_opt']
|
||||
ipv4_cidr_pattern = '192.168.{}.0/24'
|
||||
ipv6_cidr_pattern = '2001:{:x}::/64'
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
super(ExtraDhcpOptionsTest, cls).resource_setup()
|
||||
cls.rand_name = data_utils.rand_name(
|
||||
cls.__name__.rsplit('.', 1)[-1])
|
||||
cls.keypair = cls.create_keypair(name=cls.rand_name)
|
||||
cls.security_group = cls.create_security_group(name=cls.rand_name)
|
||||
cls.create_loginable_secgroup_rule(
|
||||
cls.security_group['id'], client=cls.client)
|
||||
|
||||
if CONF.neutron_plugin_options.default_image_is_advanced:
|
||||
cls.flavor_ref = CONF.compute.flavor_ref
|
||||
cls.image_ref = CONF.compute.image_ref
|
||||
cls.username = CONF.validation.image_ssh_user
|
||||
else:
|
||||
cls.flavor_ref = \
|
||||
CONF.neutron_plugin_options.advanced_image_flavor_ref
|
||||
cls.image_ref = CONF.neutron_plugin_options.advanced_image_ref
|
||||
cls.username = CONF.neutron_plugin_options.advanced_image_ssh_user
|
||||
|
||||
if (not cls.flavor_ref) or (not cls.image_ref):
|
||||
raise cls.skipException(
|
||||
'No advance image/flavor available for these tests')
|
||||
|
||||
def setUp(self):
|
||||
super(ExtraDhcpOptionsTest, self).setUp()
|
||||
self.subnet = None
|
||||
self.port = None
|
||||
self.server = None
|
||||
self.dhcp_disabled = {}
|
||||
|
||||
def _check_created_extra_dhcp_opts(self):
|
||||
# obtain the extra_dhcp_opts from Neutron API
|
||||
retrieved = self.client.show_port(
|
||||
self.port['id'])['port']['extra_dhcp_opts']
|
||||
self.assertEqual(len(retrieved), len(self.extra_dhcp_opts))
|
||||
for retrieved_option in retrieved:
|
||||
for option in self.extra_dhcp_opts:
|
||||
# default ip_version is 4
|
||||
ip_version = option.get('ip_version', IPV4)
|
||||
if (retrieved_option['opt_value'] == option['opt_value'] and
|
||||
retrieved_option['opt_name'] == option['opt_name'] and
|
||||
retrieved_option['ip_version'] == ip_version):
|
||||
break
|
||||
else:
|
||||
self.fail('Extra DHCP option not found in port %s' %
|
||||
str(retrieved_option))
|
||||
|
||||
def _create_port_and_check_dhcp_opts(
|
||||
self,
|
||||
ipv6=False,
|
||||
ra_address_mode=None,
|
||||
dhcp4_enabled=True,
|
||||
dhcp6_enabled=True):
|
||||
rand_name_test = self.rand_name + '-' + self._testMethodName
|
||||
network = self.create_network(name=rand_name_test)
|
||||
|
||||
subnet_index = len(self.reserved_subnet_cidrs)
|
||||
self.subnet = self.create_subnet(
|
||||
network=network,
|
||||
cidr=self.ipv4_cidr_pattern.format(subnet_index),
|
||||
name=rand_name_test,
|
||||
enable_dhcp=dhcp4_enabled)
|
||||
router = self.create_router_by_client()
|
||||
self.create_router_interface(router['id'], self.subnet['id'])
|
||||
if ipv6:
|
||||
if dhcp6_enabled:
|
||||
subnetv6 = self.create_subnet(
|
||||
network=network,
|
||||
ip_version=IPV6,
|
||||
name=rand_name_test + '-ipv6',
|
||||
cidr=self.ipv6_cidr_pattern.format(subnet_index),
|
||||
ipv6_ra_mode=ra_address_mode,
|
||||
ipv6_address_mode=ra_address_mode)
|
||||
else:
|
||||
# when dhcp6 is disabled, neither ipv6_ra_mode nor
|
||||
# ipv6_address_mode should be provided
|
||||
subnetv6 = self.create_subnet(
|
||||
network=network,
|
||||
ip_version=IPV6,
|
||||
name=rand_name_test + '-ipv6',
|
||||
cidr=self.ipv6_cidr_pattern.format(subnet_index),
|
||||
enable_dhcp=dhcp6_enabled)
|
||||
self.create_router_interface(router['id'], subnetv6['id'])
|
||||
|
||||
# a port is created with the extra_dhcp_options from the test case
|
||||
self.port = self.create_port(
|
||||
network=network, name=rand_name_test,
|
||||
security_groups=[self.security_group['id']],
|
||||
extra_dhcp_opts=self.extra_dhcp_opts)
|
||||
|
||||
self._check_created_extra_dhcp_opts()
|
||||
|
||||
def _create_server_and_fip(self):
|
||||
networks = [{'port': self.port['id']}]
|
||||
# config_drive is needed when dhcp4 is disabled in order to provide
|
||||
# an IPv4 to the server to be able to connect via ssh to it
|
||||
config_drive = not self.subnet['enable_dhcp']
|
||||
self.server = self.create_server(
|
||||
flavor_ref=self.flavor_ref,
|
||||
image_ref=self.image_ref,
|
||||
key_name=self.keypair['name'],
|
||||
networks=networks,
|
||||
config_drive=config_drive,
|
||||
name=self.rand_name + '-' + self._testMethodName)['server']
|
||||
|
||||
vm_fip = self.create_floatingip(port=self.port)['floating_ip_address']
|
||||
return ssh.Client(vm_fip,
|
||||
self.username,
|
||||
pkey=self.keypair['private_key'])
|
||||
|
||||
def _test_extra_dhcp_opts_ipv4_ipv6(self, ra_address_mode):
|
||||
self._create_port_and_check_dhcp_opts(
|
||||
ipv6=True, ra_address_mode=ra_address_mode)
|
||||
vm_ssh_client = self._create_server_and_fip()
|
||||
|
||||
nmcli_dhcp_options = {}
|
||||
nmcli_dhcp_options[IPV4] = utils.parse_dhcp_options_from_nmcli(
|
||||
vm_ssh_client, IPV4)
|
||||
nmcli_dhcp_options[IPV6] = utils.parse_dhcp_options_from_nmcli(
|
||||
vm_ssh_client, IPV6, timeout=100)
|
||||
|
||||
for extra_dhcp_opt in self.extra_dhcp_opts:
|
||||
ip_version = extra_dhcp_opt.get('ip_version', IPV4)
|
||||
|
||||
# if opt_name is an integer value, then we need to obtain its
|
||||
# corresponding option name
|
||||
try:
|
||||
int(extra_dhcp_opt['opt_name'])
|
||||
except ValueError:
|
||||
# it is not an integer
|
||||
opt_name = extra_dhcp_opt['opt_name']
|
||||
else:
|
||||
opt_name = constants.DHCP_OPTIONS_NUMBER_TO_NAME[
|
||||
extra_dhcp_opt['opt_name']]
|
||||
|
||||
self.assertIn(opt_name, nmcli_dhcp_options[ip_version])
|
||||
# some formatting is needed before the value comparison:
|
||||
# - remove extra " from Neutron option
|
||||
# - remove extra \ from nmcli option
|
||||
self.assertEqual(
|
||||
extra_dhcp_opt['opt_value'].replace('"', ''),
|
||||
nmcli_dhcp_options[ip_version][opt_name].replace('\\', ''))
|
||||
|
||||
@decorators.idempotent_id('8f52b4dc-faae-4f1d-b113-d2f3e86bf0d6')
|
||||
def test_extra_dhcp_opts_ipv4_ipv6_stateful(self):
|
||||
self.extra_dhcp_opts = [
|
||||
{'opt_value': '2001:4860:4860::8888',
|
||||
'opt_name': 'dns-server',
|
||||
'ip_version': IPV6},
|
||||
{'opt_value': '8.8.8.8',
|
||||
'opt_name': 'dns-server',
|
||||
'ip_version': IPV4},
|
||||
{'opt_value': '1600',
|
||||
'opt_name': '26'}] # 26 is option code for mtu
|
||||
|
||||
self._test_extra_dhcp_opts_ipv4_ipv6('dhcpv6-stateful')
|
||||
|
||||
@decorators.idempotent_id('e9e32249-6148-4565-b7b1-e64c77c9f4ec')
|
||||
def test_extra_dhcp_opts_ipv4_ipv6_stateless(self):
|
||||
self.extra_dhcp_opts = [
|
||||
{'opt_value': '"ipv6.domain"', # domains should be between "
|
||||
'opt_name': 'domain-search',
|
||||
'ip_version': IPV6},
|
||||
{'opt_value': '"ipv4.domain"', # ditto
|
||||
'opt_name': 'domain-name',
|
||||
'ip_version': IPV4}]
|
||||
|
||||
self._test_extra_dhcp_opts_ipv4_ipv6('dhcpv6-stateless')
|
||||
|
||||
@decorators.idempotent_id('ef41d6d8-f2bf-44e4-9f4d-bb8a3fed50ad')
|
||||
def test_extra_dhcp_opts_disabled_enabled_dhcp4(self):
|
||||
domain_value = 'ipv4.domain'
|
||||
domain_opt = 'domain-name'
|
||||
self.extra_dhcp_opts = [
|
||||
{'opt_value': '"{}"'.format(domain_value),
|
||||
'opt_name': domain_opt,
|
||||
'ip_version': IPV4}]
|
||||
|
||||
# dhcp is disabled from the iPv4 subnet created
|
||||
self._create_port_and_check_dhcp_opts(dhcp4_enabled=False)
|
||||
vm_ssh_client = self._create_server_and_fip()
|
||||
# ipv4.domain is not expected
|
||||
vm_resolv_conf = vm_ssh_client.exec_command(
|
||||
"sudo dhclient && cat /etc/resolv.conf")
|
||||
self.assertIsNone(re.search(r'^search\s+{}$'.format(domain_value),
|
||||
vm_resolv_conf,
|
||||
re.MULTILINE))
|
||||
|
||||
# enable dhcp for the IPv4 subnet and reboot the VM
|
||||
self.subnets[-1] = self.client.update_subnet(
|
||||
self.subnets[-1]['id'], enable_dhcp=True)['subnet']
|
||||
self.os_primary.servers_client.reboot_server(
|
||||
self.server['id'], type='SOFT')
|
||||
waiters.wait_for_server_status(self.os_primary.servers_client,
|
||||
self.server['id'],
|
||||
neutron_constants.SERVER_STATUS_ACTIVE)
|
||||
# ipv4.domain is expected
|
||||
vm_resolv_conf = vm_ssh_client.exec_command(
|
||||
"sudo dhclient && cat /etc/resolv.conf")
|
||||
self.assertIsNotNone(re.search(r'^search\s+{}$'.format(domain_value),
|
||||
vm_resolv_conf,
|
||||
re.MULTILINE))
|
||||
|
||||
@decorators.idempotent_id('abb12899-690a-407d-99d4-49eca030ce94')
|
||||
def test_extra_dhcp_opts_disabled_dhcp6(self):
|
||||
domain_value = 'ipv6.domain'
|
||||
domain_opt = 'domain-search'
|
||||
self.extra_dhcp_opts = [
|
||||
{'opt_value': '"{}"'.format(domain_value),
|
||||
'opt_name': domain_opt,
|
||||
'ip_version': IPV6}]
|
||||
|
||||
# dhcp is disabled from the iPv6 subnet created
|
||||
self._create_port_and_check_dhcp_opts(ipv6=True, dhcp6_enabled=False)
|
||||
vm_ssh_client = self._create_server_and_fip()
|
||||
|
||||
# empty nmcli_dhcp6_options is expected
|
||||
nmcli_dhcp6_options = utils.parse_dhcp_options_from_nmcli(
|
||||
vm_ssh_client, IPV6, timeout=100, expected_empty=True)
|
||||
self.assertIsNone(nmcli_dhcp6_options)
|
||||
|
||||
|
||||
class OvnExtraDhcpOptionsTest(ExtraDhcpOptionsTest,
|
||||
ds_base.BaseTempestTestCaseOvn):
|
||||
def _check_created_extra_dhcp_opts(self):
|
||||
def _check_options_from_ovn_nbdb(dhcp_option_uuids):
|
||||
cmd2_pattern = '{} get dhcp_options {} options:{}'
|
||||
|
||||
# are dhcpv4 and dhcpv6 enabled?
|
||||
subnets_dhcp_enabled = {subnet['ip_version']: subnet['enable_dhcp']
|
||||
for subnet in self.subnets}
|
||||
|
||||
for extra_dhcp_opt in self.extra_dhcp_opts:
|
||||
ip_version = extra_dhcp_opt.get('ip_version', IPV4)
|
||||
if (not subnets_dhcp_enabled[ip_version] or
|
||||
self.dhcp_disabled.get(ip_version) is True or
|
||||
extra_dhcp_opt['opt_name'] == 'dhcp_disabled'):
|
||||
continue
|
||||
ovn_opt_name = constants.DHCP_OPTIONS_NEUTRON_TO_OVN[
|
||||
extra_dhcp_opt['opt_name']]
|
||||
# if the option is not found in the OVN DB, the command will
|
||||
# fail and then the test will fail, which is correct
|
||||
ovn_opt_value = self.run_on_master_controller(
|
||||
cmd2_pattern.format(
|
||||
self.nbctl,
|
||||
dhcp_option_uuids[ip_version],
|
||||
ovn_opt_name))
|
||||
# some formatting is needed before the value comparison:
|
||||
# - remove extra " from Neutron option
|
||||
# - remove extra \ and " from OVN option
|
||||
self.assertEqual(
|
||||
extra_dhcp_opt['opt_value'].replace('"', ''),
|
||||
ovn_opt_value.replace('"', '').replace('\\', '').strip())
|
||||
|
||||
# Verify no dhcpvX_option_uuid exist when DHCPvX is disabled
|
||||
# Value '[]' means no entry in OVN NBDB dhcp_options table
|
||||
for ip_version in subnets_dhcp_enabled:
|
||||
if (subnets_dhcp_enabled[ip_version] is False or
|
||||
self.dhcp_disabled.get(ip_version) is True):
|
||||
self.assertEqual('[]', dhcp_option_uuids[ip_version])
|
||||
|
||||
# first, call parent's method
|
||||
super(OvnExtraDhcpOptionsTest, self)._check_created_extra_dhcp_opts()
|
||||
|
||||
cmd1_pattern = '{} find logical_switch_port name={}'
|
||||
output = self.run_on_master_controller(
|
||||
cmd1_pattern.format(self.nbctl, self.port['id']))
|
||||
dhcp_option_uuids = {}
|
||||
|
||||
for line in output.splitlines():
|
||||
if re.search(r'^dhcpv4_options', line):
|
||||
ip_version = IPV4
|
||||
elif re.search(r'^dhcpv6_options', line):
|
||||
ip_version = IPV6
|
||||
else:
|
||||
# skip this line
|
||||
continue
|
||||
dhcp_option_uuids[ip_version] = re.sub(
|
||||
r'^dhcpv{}_options\s+: '.format(ip_version),
|
||||
'',
|
||||
line).strip()
|
||||
|
||||
_check_options_from_ovn_nbdb(dhcp_option_uuids)
|
||||
|
||||
@decorators.idempotent_id('30ef3221-e46b-4358-b550-d98c08272e1c')
|
||||
def test_extra_dhcp_opts_dhcp_disabled_option(self):
|
||||
self.extra_dhcp_opts = [
|
||||
{'opt_value': '2001:4860:4860::8888',
|
||||
'opt_name': 'dns-server',
|
||||
'ip_version': IPV6},
|
||||
{'opt_value': '8.8.8.8',
|
||||
'opt_name': 'dns-server',
|
||||
'ip_version': IPV4},
|
||||
{'opt_value': 'True',
|
||||
'opt_name': 'dhcp_disabled',
|
||||
'ip_version': IPV6},
|
||||
{'opt_value': 'False',
|
||||
'opt_name': 'dhcp_disabled',
|
||||
'ip_version': IPV4}]
|
||||
self.dhcp_disabled[IPV6] = True
|
||||
self.dhcp_disabled[IPV4] = False
|
||||
|
||||
# create port and check extra-dhcp-opts
|
||||
self._create_port_and_check_dhcp_opts(
|
||||
ipv6=True, ra_address_mode='dhcpv6-stateless')
|
||||
|
||||
# update port and check extra-dhcp-opts
|
||||
self.extra_dhcp_opts = [
|
||||
{'opt_value': '2001:4860:4860::8888',
|
||||
'opt_name': 'dns-server',
|
||||
'ip_version': IPV6},
|
||||
{'opt_value': '8.8.8.8',
|
||||
'opt_name': 'dns-server',
|
||||
'ip_version': IPV4},
|
||||
{'opt_value': 'False',
|
||||
'opt_name': 'dhcp_disabled',
|
||||
'ip_version': IPV6},
|
||||
{'opt_value': 'True',
|
||||
'opt_name': 'dhcp_disabled',
|
||||
'ip_version': IPV4}]
|
||||
self.dhcp_disabled[IPV6] = False
|
||||
self.dhcp_disabled[IPV4] = True
|
||||
self.port = self.client.update_port(
|
||||
self.port['id'], extra_dhcp_opts=self.extra_dhcp_opts)['port']
|
||||
self._check_created_extra_dhcp_opts()
|
@ -24,14 +24,15 @@
|
||||
USE_PYTHON3: true
|
||||
NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_tempest) | join(',') }}"
|
||||
PHYSICAL_NETWORK: public
|
||||
IMAGE_URLS: https://cloud-images.ubuntu.com/minimal/releases/focal/release/ubuntu-20.04-minimal-cloudimg-amd64.img
|
||||
IMAGE_URLS: https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2
|
||||
CIRROS_VERSION: 0.6.2
|
||||
DEFAULT_IMAGE_NAME: cirros-0.6.2-x86_64-uec
|
||||
DEFAULT_IMAGE_FILE_NAME: cirros-0.6.2-x86_64-uec.tar.gz
|
||||
ADVANCED_IMAGE_NAME: ubuntu-20.04-minimal-cloudimg-amd64
|
||||
ADVANCED_INSTANCE_TYPE: ntp_image_256M
|
||||
ADVANCED_INSTANCE_USER: ubuntu
|
||||
CUSTOMIZE_IMAGE: true
|
||||
ADVANCED_IMAGE_NAME: Rocky-9-GenericCloud.latest.x86_64
|
||||
ADVANCED_INSTANCE_TYPE: ds1G
|
||||
ADVANCED_INSTANCE_USER: rocky
|
||||
GLANCE_LIMIT_IMAGE_SIZE_TOTAL: 2000
|
||||
CUSTOMIZE_IMAGE: false
|
||||
BUILD_TIMEOUT: 784
|
||||
# TODO(lucasagomes): Re-enable MOD_WSGI after
|
||||
# https://bugs.launchpad.net/neutron/+bug/1912359 is implemented
|
||||
|
Loading…
Reference in New Issue
Block a user