diff --git a/plugin_test/vapor/create_nat_image.sh b/plugin_test/vapor/create_nat_image.sh new file mode 100644 index 000000000..cb85a50fe --- /dev/null +++ b/plugin_test/vapor/create_nat_image.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Script to create NAT service image from ubuntu image + +guestfish << _EOF_ +add $IMAGE +run +mount /dev/sda1 / +download /etc/sysctl.conf /tmp/sysctl.conf +! echo "net.ipv4.ip_forward = 1" >> /tmp/sysctl.conf +upload /tmp/sysctl.conf /etc/sysctl.conf +! echo "dhclient" > /tmp/rc.local +! echo "/sbin/iptables -t nat -A POSTROUTING -o eth2 -j MASQUERADE" >> /tmp/rc.local +! echo "/sbin/iptables -A FORWARD -i eth2 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT" >> /tmp/rc.local +! echo "/sbin/iptables -A FORWARD -i eth1 -o eth2 -j ACCEPT" >> /tmp/rc.local +! echo "exit 0" >> /tmp/rc.local +upload /tmp/rc.local /etc/rc.local +echo "Set root password to 'password'" +download /etc/shadow /tmp/shadow +! sed -i 's/^root:[^:]\+:/root:$1$h2thgSpv$lAm04bPzOoldW8H0EVwVA0:/' /tmp/shadow +upload /tmp/shadow /etc/shadow +echo "Permit root login" +download /etc/ssh/sshd_config /tmp/sshd_config +! sed -i 's/^PasswordAuthentication.*$/PasswordAuthentication yes/' /tmp/sshd_config +! sed -i 's/^PermitRootLogin.*$/PermitRootLogin yes/' /tmp/sshd_config +upload /tmp/sshd_config /etc/ssh/sshd_config +_EOF_ diff --git a/plugin_test/vapor/vapor/conftest.py b/plugin_test/vapor/vapor/conftest.py index 939e0d7d9..7210a766f 100644 --- a/plugin_test/vapor/vapor/conftest.py +++ b/plugin_test/vapor/vapor/conftest.py @@ -10,12 +10,14 @@ from vapor.fixtures.contrail import * # noqa from vapor.fixtures.contrail_resources import * # noqa from vapor.fixtures.different_tenants_resources import * # noqa from vapor.fixtures.dns import * # noqa +from vapor.fixtures.images import * # noqa from vapor.fixtures.instance_ip import * # noqa from vapor.fixtures.ipams import * # noqa from vapor.fixtures.networks import * # noqa from vapor.fixtures.nodes import * # noqa from vapor.fixtures.policies import * # noqa from vapor.fixtures.security_groups import * # noqa +from vapor.fixtures.service_chain import * # noqa from vapor.fixtures.skip import * # noqa from vapor.fixtures.subnets import * # noqa from vapor.fixtures.system_services import * # noqa diff --git a/plugin_test/vapor/vapor/fixtures/images.py b/plugin_test/vapor/vapor/fixtures/images.py new file mode 100644 index 000000000..1367f852b --- /dev/null +++ b/plugin_test/vapor/vapor/fixtures/images.py @@ -0,0 +1,39 @@ +# 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 pytest +from stepler.glance.fixtures import images +from stepler.third_party import utils + +from vapor import settings + + +@pytest.fixture(scope='session') +def nat_service_image(get_glance_steps, uncleanable, credentials): + """Session fixture to create ubuntu image. + Creates image from config.UBUNTU_QCOW2_URL with default options. + + Args: + get_glance_steps (function): function to get glance steps + uncleanable (AttrDict): data structure with skipped resources + credentials (object): CredentialsManager instance + + Returns: + object: ubuntu glance image + """ + with images.create_images_context( + get_glance_steps, + uncleanable, + credentials, + utils.generate_ids('nat'), + settings.NAT_SERVICE_IMAGE_URL) as created_images: + yield created_images[0] diff --git a/plugin_test/vapor/vapor/fixtures/service_chain.py b/plugin_test/vapor/vapor/fixtures/service_chain.py new file mode 100644 index 000000000..350a5d44c --- /dev/null +++ b/plugin_test/vapor/vapor/fixtures/service_chain.py @@ -0,0 +1,82 @@ +# 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 functools + +import pycontrail.types as types +import pytest +from stepler import config as stepler_config +from stepler.third_party import utils + +from vapor.helpers import service_chain + + +@pytest.fixture +def add_neutron_user_to_project(current_project, role_steps, user_steps): + """Workaround to allow contrail to boot service instances.""" + neutron_user = user_steps.get_user(name='neutron') + role = role_steps.get_role(name=stepler_config.ROLE_MEMBER) + role_steps.grant_role(role, neutron_user, project=current_project) + + +@pytest.fixture +def service_template(contrail_api_client, nat_service_image, flavor): + name = next(utils.generate_ids('nat_template')) + interface_types = [ + types.ServiceTemplateInterfaceType( + service_interface_type='management'), + types.ServiceTemplateInterfaceType(service_interface_type='left'), + types.ServiceTemplateInterfaceType(service_interface_type='right'), + ] + template_properties = types.ServiceTemplateType( + version=1, + service_mode=u'in-network', + service_type=u'firewall', + image_name=nat_service_image.name, + flavor=flavor.name, + interface_type=interface_types, + ordered_interfaces=True, + service_virtualization_type=u'virtual-machine') + template = types.ServiceTemplate( + name=name, service_template_properties=template_properties) + contrail_api_client.service_template_create(template) + yield template + contrail_api_client.service_template_delete(id=template.uuid) + + +@pytest.fixture +def service_instance(request, contrail_api_client, contrail_current_project, + service_template, contrail_2_networks, server_steps, + add_neutron_user_to_project): + left_vn, right_vn = contrail_2_networks.networks + left_fq_name = ':'.join(left_vn['contrail:fq_name']) + right_fq_name = ':'.join(right_vn['contrail:fq_name']) + name = next(utils.generate_ids('nat_instance')) + instance_properties = types.ServiceInstanceType( + scale_out=types.ServiceScaleOutType(1), + management_virtual_network='', + left_virtual_network=left_fq_name, + right_virtual_network=right_fq_name) + instance = types.ServiceInstance( + name=name, + parent_obj=contrail_current_project, + service_instance_properties=instance_properties) + instance.set_service_template(service_template) + contrail_api_client.service_instance_create(instance) + + request.addfinalizer( + functools.partial(service_chain.delete_service_instance, + contrail_api_client, instance, server_steps)) + + service_chain.check_service_instance_ready(contrail_api_client, instance, + server_steps) + return instance diff --git a/plugin_test/vapor/vapor/helpers/service_chain.py b/plugin_test/vapor/vapor/helpers/service_chain.py new file mode 100644 index 000000000..1dd4568f2 --- /dev/null +++ b/plugin_test/vapor/vapor/helpers/service_chain.py @@ -0,0 +1,67 @@ +# 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 stepler import config as stepler_config +from stepler.third_party import waiter + +from vapor import settings + + +def check_service_instance_ready(contrail_api_client, service_instance, + server_steps): + """Check that service instance creates nova server and it's booted.""" + + def _get_virtual_machine_uuid(): + fresh_instance = contrail_api_client.service_instance_read( + id=service_instance.uuid) + refs = fresh_instance.get_virtual_machine_back_refs() + if refs: + return refs[0]['uuid'] + + server_uuid = waiter.wait( + _get_virtual_machine_uuid, + timeout_seconds=settings.SERVICE_INSTANCE_CREATE_TIMEOUT) + + server = next(server for server in server_steps.get_servers() + if server.id == server_uuid) + + server_steps.check_server_status( + server, + expected_statuses=[stepler_config.STATUS_ACTIVE], + transit_statuses=[stepler_config.STATUS_BUILD], + timeout=stepler_config.SERVER_ACTIVE_TIMEOUT) + + def _check_record_in_log(): + server.get() + console = server.get_console_output() + return re.search(settings.SERVICE_INSTANCE_BOOT_DONE_PATTERN, console) + + waiter.wait( + _check_record_in_log, + timeout_seconds=settings.SERVICE_INSTANCE_BOOT_TIMEOUT) + + +def delete_service_instance(contrail_api_client, service_instance, + server_steps): + """"Delete service instance.""" + refs = service_instance.get_virtual_machine_back_refs() + contrail_api_client.service_instance_delete(id=service_instance.uuid) + if refs: + server_uuid = refs[0]['uuid'] + server = next(server for server in server_steps.get_servers() + if server.id == server_uuid) + server_steps.check_server_presence( + server, + present=False, + timeout=stepler_config.SERVER_DELETE_TIMEOUT) diff --git a/plugin_test/vapor/vapor/settings.py b/plugin_test/vapor/vapor/settings.py index bc1c517e5..91455faa6 100644 --- a/plugin_test/vapor/vapor/settings.py +++ b/plugin_test/vapor/vapor/settings.py @@ -220,3 +220,13 @@ SECURITY_GROUP_SSH_PING_RULES = (stepler_config.SECURITY_GROUP_SSH_RULES + SECURITY_GROUP_PING_RULES) DPDK_ENABLED_GROUP = u'Network devices using DPDK-compatible driver' + + + +# Service chaining +# TODO(gdyuldin): relace with real URL +NAT_SERVICE_IMAGE_URL = os.environ.get('NAT_SERVICE_IMAGE_URL', + '/home/jenkins/nat.qcow2') +SERVICE_INSTANCE_CREATE_TIMEOUT = 2 * 60 +SERVICE_INSTANCE_BOOT_TIMEOUT = 10 * 60 +SERVICE_INSTANCE_BOOT_DONE_PATTERN = 'Cloud-init .+ finished' \ No newline at end of file diff --git a/plugin_test/vapor/vapor/tests/test_service_instance.py b/plugin_test/vapor/vapor/tests/test_service_instance.py new file mode 100644 index 000000000..6988bb373 --- /dev/null +++ b/plugin_test/vapor/vapor/tests/test_service_instance.py @@ -0,0 +1,156 @@ +# 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 time + +from hamcrest import assert_that, all_of, contains_string, is_not +import pycontrail.types as types +from stepler import config as stepler_config + + +def _get_tcpdump_log_with_ping(server_steps, left_vm, right_vm, left_vm_fip, + right_vm_fip, right_vm_fixed_ip): + """Start ping from left to right vm, returns right vm tcpdump's log.""" + log_file = '/tmp/tcpdump' + with server_steps.get_server_ssh( + right_vm, right_vm_fip['floating_ip_address']) as right_ssh: + with right_ssh.sudo(): + right_ssh.check_call('apt-get install -y tcpdump') + with server_steps.get_server_ssh( + left_vm, left_vm_fip['floating_ip_address']) as left_ssh: + with server_steps.check_ping_loss_context( + right_vm_fixed_ip, server_ssh=left_ssh): + tcpdump_pid = right_ssh.background_call( + 'tcpdump -i eth0 icmp', stdout=log_file) + time.sleep(10) + right_ssh.check_call('kill {}'.format(tcpdump_pid)) + return right_ssh.check_call('cat {}'.format(log_file)).stdout + + +def test_nat_service_instance(flavor, ubuntu_image, keypair, public_network, + neutron_security_group, contrail_2_networks, + create_floating_ip, contrail_network_policy, + set_network_policy, contrail_api_client, + server_steps, port_steps, service_instance): + """Test contrail NAT service chain. + + Steps: + #. Create flavor + #. Create ubuntu image + #. Create keypair + #. Create security group + #. Create left and right networks with subnets + #. Create service template + #. Create service instance with NAT service + #. Create policy + #. Assign policy to networks + #. Create left and right vm on corresponding networks + #. Start ping from left to right vm + #. Check that left vm ip is present on tcpdump output on right vm + #. Stop ping + #. Add service instance to policy rule + #. Start ping from left to right vm + #. Check that left vm ip is absent on tcpdump output on right vm and + right interface ip of service instance is present on tcpdump output + #. Stop ping + """ + # Add security group to NAT instance + nat_vm_id = service_instance.get_virtual_machine_back_refs()[0]['uuid'] + nova_nat_instance = next(server for server in server_steps.get_servers() + if server.id == nat_vm_id) + nova_nat_instance.add_security_group(neutron_security_group['id']) + + left_vn, right_vn = [ + contrail_api_client.virtual_network_read(id=net['id']) + for net in contrail_2_networks.networks + ] + + # Create policy rule with service + rule = types.PolicyRuleType( + action_list=types.ActionListType(simple_action='pass'), + direction='<>', + protocol='any', + src_addresses=[ + types.AddressType(virtual_network=left_vn.get_fq_name_str()) + ], + src_ports=[types.PortType()], + dst_addresses=[ + types.AddressType(virtual_network=right_vn.get_fq_name_str()) + ], + dst_ports=[types.PortType()]) + contrail_network_policy.set_network_policy_entries( + types.PolicyEntriesType(policy_rule=[rule])) + contrail_api_client.network_policy_update(contrail_network_policy) + + # Assign policy to networks + set_network_policy(left_vn, contrail_network_policy) + set_network_policy(right_vn, contrail_network_policy) + + # Create 2 servers + servers = [] + floating_ips = [] + for net in contrail_2_networks.networks: + server = server_steps.create_servers( + flavor=flavor, + image=ubuntu_image, + networks=[net], + security_groups=[neutron_security_group], + keypair=keypair, + username=stepler_config.UBUNTU_USERNAME)[0] + + servers.append(server) + + server_port = port_steps.get_port( + device_owner=stepler_config.PORT_DEVICE_OWNER_SERVER, + device_id=server.id) + floating_ip = create_floating_ip(public_network, port=server_port) + + floating_ips.append(floating_ip) + + left_vm, right_vm = servers + left_vm_fip, right_vm_fip = floating_ips + + # Install tcpdump on right vm + with server_steps.get_server_ssh( + right_vm, right_vm_fip['floating_ip_address']) as right_ssh: + with right_ssh.sudo(): + right_ssh.check_call('apt-get install -y tcpdump') + + right_vm_fixed_ip = server_steps.get_fixed_ip(right_vm) + nat_right_ip = nova_nat_instance.addresses[right_vn.name][0]['addr'] + left_vm_fixed_ip = server_steps.get_fixed_ip(left_vm) + + # Start ping from left to right server and tcpdump on right server + tcpdump_stdout = _get_tcpdump_log_with_ping( + server_steps, left_vm, right_vm, left_vm_fip, right_vm_fip, + right_vm_fixed_ip) + + # Check that ICMP packets contains left vm ip address + assert_that(tcpdump_stdout, contains_string(left_vm_fixed_ip)) + + # Update policy rule + rule.action_list.apply_service = [service_instance.get_fq_name_str()] + contrail_network_policy.set_network_policy_entries( + types.PolicyEntriesType(policy_rule=[rule])) + contrail_api_client.network_policy_update(contrail_network_policy) + + # Start ping from left to right server and tcpdump on right server + tcpdump_stdout = _get_tcpdump_log_with_ping( + server_steps, left_vm, right_vm, left_vm_fip, right_vm_fip, + right_vm_fixed_ip) + + # Check that ICMP packets has NAT right ip address and no contains left + # vm ip address + assert_that(tcpdump_stdout, + all_of( + contains_string(nat_right_ip), + is_not(contains_string(left_vm_fixed_ip))))