From 4f5dda873762438b3ba296da289dae5f3312c9da Mon Sep 17 00:00:00 2001 From: Maximilian Stinsky Date: Fri, 19 Jul 2024 12:35:19 +0200 Subject: Implement neutron-ovn-vpn-agent This patch implements vpnaas for OVN environments. It deploys a standalone neutron-ovn-vpn-agent on neutron nodes. The container no longer requires 'privileged: true'. Running as the 'neutron' user with specific kernel capabilities ensures the agent can manage network namespaces and interfaces without full privileges. Added capabilities: - NET_ADMIN # manage interfaces, IPs, routing, firewall - SYS_ADMIN # create/manage network namespaces - SYS_PTRACE # allow 'ip netns exec' attach to processes - DAC_OVERRIDE # bypass file read/write restrictions - DAC_READ_SEARCH # allow directory traversal and file read - SETPCAP # retain capabilities for privileged ops Added security options: - apparmor=unconfined # disable AppArmor confinement - seccomp=unconfined # disable seccomp filtering - label=disable # disable SELinux/labeling Depends-On: https://review.opendev.org/c/openstack/kolla/+/924302 Closes-Bug: #2048392 Change-Id: I961b1407ae1402d146f0c09924df8e007e331af5 Signed-off-by: Joachim de Groot Signed-off-by: Piotr Milewski --- ansible/inventory/all-in-one | 3 + ansible/inventory/multinode | 3 + ansible/roles/neutron/defaults/main.yml | 54 +++++- ansible/roles/neutron/handlers/main.yml | 21 ++ ansible/roles/neutron/tasks/config.yml | 16 ++ .../templates/neutron-ovn-vpn-agent.json.j2 | 41 ++++ .../neutron/templates/neutron_ovn_vpn_agent.ini.j2 | 17 ++ .../roles/neutron/templates/neutron_vpnaas.conf.j2 | 4 +- .../reference/networking/neutron-extensions.rst | 41 ++-- ...ent-neutron-ovn-vpn-agent-b848fb4c245e415e.yaml | 6 + roles/openstack-clients/defaults/main.yml | 2 + roles/test-ovn-vpnaas/defaults/main.yml | 14 ++ roles/test-ovn-vpnaas/tasks/cleanup_site.yml | 46 +++++ roles/test-ovn-vpnaas/tasks/init_env.yml | 33 ++++ roles/test-ovn-vpnaas/tasks/main.yml | 211 +++++++++++++++++++++ roles/test-ovn-vpnaas/tasks/setup_site.yml | 76 ++++++++ tests/run.yml | 8 + tests/templates/globals-default.j2 | 1 + tests/templates/inventory.j2 | 3 + tools/init-vpn | 139 ++++++++++---- 20 files changed, 684 insertions(+), 55 deletions(-) create mode 100644 ansible/roles/neutron/templates/neutron-ovn-vpn-agent.json.j2 create mode 100644 ansible/roles/neutron/templates/neutron_ovn_vpn_agent.ini.j2 create mode 100644 releasenotes/notes/implement-neutron-ovn-vpn-agent-b848fb4c245e415e.yaml create mode 100644 roles/test-ovn-vpnaas/defaults/main.yml create mode 100644 roles/test-ovn-vpnaas/tasks/cleanup_site.yml create mode 100644 roles/test-ovn-vpnaas/tasks/init_env.yml create mode 100644 roles/test-ovn-vpnaas/tasks/main.yml create mode 100644 roles/test-ovn-vpnaas/tasks/setup_site.yml diff --git a/ansible/inventory/all-in-one b/ansible/inventory/all-in-one index 79704c8de..4a8419b58 100644 --- a/ansible/inventory/all-in-one +++ b/ansible/inventory/all-in-one @@ -239,6 +239,9 @@ neutron [neutron-metadata-agent:children] neutron +[neutron-ovn-vpn-agent:children] +neutron + [neutron-metering-agent:children] neutron diff --git a/ansible/inventory/multinode b/ansible/inventory/multinode index cbb0db70c..e90848e71 100644 --- a/ansible/inventory/multinode +++ b/ansible/inventory/multinode @@ -280,6 +280,9 @@ control compute network +[neutron-ovn-vpn-agent:children] +neutron + # Cinder [cinder-api:children] cinder diff --git a/ansible/roles/neutron/defaults/main.yml b/ansible/roles/neutron/defaults/main.yml index 0518e3d8d..206320a24 100644 --- a/ansible/roles/neutron/defaults/main.yml +++ b/ansible/roles/neutron/defaults/main.yml @@ -215,6 +215,29 @@ neutron_services: volumes: "{{ neutron_ovn_agent_default_volumes + neutron_ovn_agent_extra_volumes }}" dimensions: "{{ neutron_ovn_agent_dimensions }}" healthcheck: "{{ neutron_ovn_agent_healthcheck }}" + neutron-ovn-vpn-agent: + container_name: "neutron_ovn_vpn_agent" + image: "{{ neutron_ovn_vpn_agent_image_full }}" + privileged: false + cgroupns_mode: "private" + pid_mode: "host" + cap_add: + - NET_ADMIN + - SYS_ADMIN + - SYS_PTRACE + - DAC_OVERRIDE + - DAC_READ_SEARCH + - SETPCAP + security_opt: + - "apparmor=unconfined" + - "seccomp=unconfined" + - "label=disable" + enabled: "{{ enable_neutron_vpnaas | bool and neutron_plugin_agent == 'ovn' }}" + group: neutron-ovn-vpn-agent + host_in_groups: "{{ inventory_hostname in groups['neutron-ovn-vpn-agent'] }}" + volumes: "{{ neutron_ovn_vpn_agent_default_volumes + neutron_ovn_vpn_agent_extra_volumes }}" + dimensions: "{{ neutron_ovn_vpn_agent_dimensions }}" + healthcheck: "{{ neutron_ovn_vpn_agent_healthcheck }}" #################### # Config Validate @@ -339,6 +362,10 @@ neutron_ovn_agent_image: "{{ docker_image_url }}neutron-ovn-agent" neutron_ovn_agent_tag: "{{ neutron_tag }}" neutron_ovn_agent_image_full: "{{ neutron_ovn_agent_image }}:{{ neutron_ovn_agent_tag }}" +neutron_ovn_vpn_agent_image: "{{ docker_image_url }}neutron-ovn-vpn-agent" +neutron_ovn_vpn_agent_tag: "{{ neutron_tag }}" +neutron_ovn_vpn_agent_image_full: "{{ neutron_ovn_vpn_agent_image }}:{{ neutron_ovn_vpn_agent_tag }}" + neutron_agent_dimensions: "{{ default_container_dimensions }}" neutron_dhcp_agent_dimensions: "{{ neutron_agent_dimensions }}" @@ -358,6 +385,7 @@ neutron_ovn_agent_dimensions: "{{ neutron_agent_dimensions }}" neutron_ovn_maintenance_worker_dimensions: "{{ default_container_dimensions }}" neutron_periodic_worker_dimensions: "{{ default_container_dimensions }}" neutron_rpc_server_dimensions: "{{ default_container_dimensions }}" +neutron_ovn_vpn_agent_dimensions: "{{ neutron_agent_dimensions }}" neutron_dhcp_agent_enable_healthchecks: "{{ enable_container_healthchecks }}" neutron_dhcp_agent_healthcheck_interval: "{{ default_container_healthcheck_interval }}" @@ -532,6 +560,19 @@ neutron_rpc_server_healthcheck: test: "{% if neutron_rpc_server_enable_healthchecks | bool %}{{ neutron_rpc_server_healthcheck_test }}{% else %}NONE{% endif %}" timeout: "{{ neutron_rpc_server_healthcheck_timeout }}" +neutron_ovn_vpn_agent_enable_healthchecks: "{{ enable_container_healthchecks }}" +neutron_ovn_vpn_agent_healthcheck_interval: "{{ default_container_healthcheck_interval }}" +neutron_ovn_vpn_agent_healthcheck_retries: "{{ default_container_healthcheck_retries }}" +neutron_ovn_vpn_agent_healthcheck_start_period: "{{ default_container_healthcheck_start_period }}" +neutron_ovn_vpn_agent_healthcheck_test: ["CMD-SHELL", "healthcheck_port neutron-ovn-vpn-agent {{ om_rpc_port }}"] +neutron_ovn_vpn_agent_healthcheck_timeout: "{{ default_container_healthcheck_timeout }}" +neutron_ovn_vpn_agent_healthcheck: + interval: "{{ neutron_ovn_vpn_agent_healthcheck_interval }}" + retries: "{{ neutron_ovn_vpn_agent_healthcheck_retries }}" + start_period: "{{ neutron_ovn_vpn_agent_healthcheck_start_period }}" + test: "{% if neutron_ovn_vpn_agent_enable_healthchecks | bool %}{{ neutron_ovn_vpn_agent_healthcheck_test }}{% else %}NONE{% endif %}" + timeout: "{{ neutron_ovn_vpn_agent_healthcheck_timeout }}" + neutron_dhcp_agent_default_volumes: - "{{ node_config_directory }}/neutron-dhcp-agent/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" @@ -648,6 +689,14 @@ neutron_rpc_server_default_volumes: - "/etc/localtime:/etc/localtime:ro" - "kolla_logs:/var/log/kolla/" - "{{ '/dev/shm:/dev/shm' }}" +neutron_ovn_vpn_agent_default_volumes: + - "{{ node_config_directory }}/neutron-ovn-vpn-agent/:{{ container_config_directory }}/:ro" + - "/etc/localtime:/etc/localtime:ro" + - "{{ '/etc/timezone:/etc/timezone:ro' if ansible_facts.os_family == 'Debian' else '' }}" + - "/run/netns:/run/netns:shared" + - "kolla_logs:/var/log/kolla/" + - "{{ '/dev/shm:/dev/shm' }}" + - "/bin/true:/usr/sbin/sysctl:ro" neutron_extra_volumes: "{{ default_extra_volumes }}" neutron_dhcp_agent_extra_volumes: "{{ neutron_extra_volumes }}" @@ -667,6 +716,7 @@ neutron_ovn_agent_extra_volumes: "{{ neutron_extra_volumes }}" neutron_ovn_maintenance_worker_extra_volumes: "{{ neutron_extra_volumes }}" neutron_periodic_worker_extra_volumes: "{{ neutron_extra_volumes }}" neutron_rpc_server_extra_volumes: "{{ neutron_extra_volumes }}" +neutron_ovn_vpn_agent_extra_volumes: "{{ neutron_extra_volumes }}" #################### # OpenStack @@ -787,7 +837,9 @@ service_plugins: - name: "trunk" enabled: "{{ enable_neutron_trunk | bool }}" - name: "vpnaas" - enabled: "{{ enable_neutron_vpnaas | bool }}" + enabled: "{{ enable_neutron_vpnaas | bool and neutron_plugin_agent != 'ovn' }}" + - name: "ovn-vpnaas" + enabled: "{{ enable_neutron_vpnaas | bool and neutron_plugin_agent == 'ovn' }}" - name: "port_forwarding" enabled: "{{ enable_neutron_port_forwarding | bool }}" - name: "ovn-router" diff --git a/ansible/roles/neutron/handlers/main.yml b/ansible/roles/neutron/handlers/main.yml index 2845fbce6..16e60fbad 100644 --- a/ansible/roles/neutron/handlers/main.yml +++ b/ansible/roles/neutron/handlers/main.yml @@ -328,3 +328,24 @@ dimensions: "{{ service.dimensions }}" privileged: "{{ service.privileged | default(False) }}" healthcheck: "{{ service.healthcheck | default(omit) }}" + +- name: Restart neutron-ovn-vpn-agent container + vars: + service_name: "neutron-ovn-vpn-agent" + service: "{{ neutron_services[service_name] }}" + become: true + kolla_container: + action: "recreate_or_restart_container" + common_options: "{{ docker_common_options }}" + environment: "{{ service.environment | default(omit) }}" + name: "{{ service.container_name }}" + image: "{{ service.image }}" + user: "{{ service.user | default(omit) }}" + cap_add: "{{ service.cap_add | default(omit) }}" + security_opt: "{{ service.security_opt | default(omit) }}" + pid_mode: "{{ service.pid_mode | default(omit) }}" + cgroupns_mode: "{{ service.cgroupns_mode | default(omit) }}" + volumes: "{{ service.volumes }}" + dimensions: "{{ service.dimensions }}" + privileged: "{{ service.privileged | default(False) }}" + healthcheck: "{{ service.healthcheck | default(omit) }}" diff --git a/ansible/roles/neutron/tasks/config.yml b/ansible/roles/neutron/tasks/config.yml index 7c1a773e4..76cf0db16 100644 --- a/ansible/roles/neutron/tasks/config.yml +++ b/ansible/roles/neutron/tasks/config.yml @@ -77,6 +77,7 @@ - "neutron-ovn-agent" - "neutron-ovn-maintenance-worker" - "neutron-ovn-metadata-agent" + - "neutron-ovn-vpn-agent" - "neutron-periodic-worker" - "neutron-rpc-server" - "neutron-server" @@ -293,6 +294,21 @@ mode: "0660" when: service | service_enabled_and_mapped_to_host +- name: Copying over neutron_ovn_vpn_agent.ini + become: true + vars: + service_name: "neutron-ovn-vpn-agent" + service: "{{ neutron_services[service_name] }}" + merge_configs: + sources: + - "{{ role_path }}/templates/neutron_ovn_vpn_agent.ini.j2" + - "{{ node_custom_config }}/neutron/neutron_ovn_vpn_agent.ini" + dest: "{{ node_config_directory }}/{{ service_name }}/neutron_ovn_vpn_agent.ini" + mode: "0660" + when: service | service_enabled_and_mapped_to_host + notify: + - "Restart {{ service_name }} container" + - name: Copying over metering_agent.ini become: true vars: diff --git a/ansible/roles/neutron/templates/neutron-ovn-vpn-agent.json.j2 b/ansible/roles/neutron/templates/neutron-ovn-vpn-agent.json.j2 new file mode 100644 index 000000000..4814642ee --- /dev/null +++ b/ansible/roles/neutron/templates/neutron-ovn-vpn-agent.json.j2 @@ -0,0 +1,41 @@ +{ + "command": "neutron-ovn-vpn-agent --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/neutron_ovn_vpn_agent.ini", + "config_files": [ + { + "source": "{{ container_config_directory }}/neutron.conf", + "dest": "/etc/neutron/neutron.conf", + "owner": "neutron", + "perm": "0600" + }, + { + "source": "{{ container_config_directory }}/neutron_ovn_vpn_agent.ini", + "dest": "/etc/neutron/neutron_ovn_vpn_agent.ini", + "owner": "neutron", + "perm": "0600" + } + {% if neutron_policy_file is defined %},{ + "source": "{{ container_config_directory }}/{{ neutron_policy_file }}", + "dest": "/etc/neutron/{{ neutron_policy_file }}", + "owner": "neutron", + "perm": "0600" + }{% endif %}{% if kolla_copy_ca_into_containers | bool %}, + { + "source": "{{ container_config_directory }}/ca-certificates", + "dest": "/var/lib/kolla/share/ca-certificates", + "owner": "root", + "perm": "0600" + }{% endif %} + ], + "permissions": [ + { + "path": "/var/log/kolla/neutron", + "owner": "neutron:neutron", + "recurse": true + }, + { + "path": "/var/lib/neutron/kolla", + "owner": "neutron:neutron", + "recurse": true + } + ] +} diff --git a/ansible/roles/neutron/templates/neutron_ovn_vpn_agent.ini.j2 b/ansible/roles/neutron/templates/neutron_ovn_vpn_agent.ini.j2 new file mode 100644 index 000000000..c2a62b97a --- /dev/null +++ b/ansible/roles/neutron/templates/neutron_ovn_vpn_agent.ini.j2 @@ -0,0 +1,17 @@ +{% set vpn_device_driver = 'OvnStrongSwanDriver' if kolla_base_distro in ['ubuntu', 'debian'] else 'OvnLibreSwanDriver' %} +[DEFAULT] +transport_url = {{ rpc_transport_url }} +interface_driver = neutron.agent.linux.interface.OVSInterfaceDriver + +[AGENT] +extensions = vpnaas + +[vpnagent] +vpn_device_driver = neutron_vpnaas.services.vpn.device_drivers.ovn_ipsec.{{ vpn_device_driver }} + +[ovs] +ovsdb_connection = tcp:127.0.0.1:{{ ovsdb_port }} +ovsdb_timeout = {{ ovsdb_timeout }} + +[ovn] +ovn_sb_connection = {{ ovn_sb_connection_no_relay }} diff --git a/ansible/roles/neutron/templates/neutron_vpnaas.conf.j2 b/ansible/roles/neutron/templates/neutron_vpnaas.conf.j2 index 01e2ec335..1ad3136d7 100644 --- a/ansible/roles/neutron/templates/neutron_vpnaas.conf.j2 +++ b/ansible/roles/neutron/templates/neutron_vpnaas.conf.j2 @@ -1,2 +1,4 @@ +{% set implementation = 'strongswan' if kolla_base_distro in ['ubuntu', 'debian'] else 'openswan' %} +{% set vpn_driver = 'VPN:' ~ implementation ~ ':neutron_vpnaas.services.vpn.service_drivers.ovn_ipsec.IPsecOvnVPNDriver:default' if neutron_plugin_agent == 'ovn' else 'VPN:openswan:neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver:default' %} [service_providers] -service_provider = VPN:openswan:neutron_vpnaas.services.vpn.service_drivers.ipsec.IPsecVPNDriver:default +service_provider = {{ vpn_driver }} diff --git a/doc/source/reference/networking/neutron-extensions.rst b/doc/source/reference/networking/neutron-extensions.rst index 2e8be4b13..e56968dd5 100644 --- a/doc/source/reference/networking/neutron-extensions.rst +++ b/doc/source/reference/networking/neutron-extensions.rst @@ -45,6 +45,13 @@ For more information on FWaaS in Neutron refer to the Neutron VPNaaS (VPN-as-a-Service) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. warning:: + + OVN VPNaaS is currently not supported on RHEL 10 based distributions + (e.g., Rocky Linux 10, CentOS Stream 10) due to an upstream bug in + Neutron. See `LP#2146308 `_ + for details. + Preparation and deployment -------------------------- @@ -60,15 +67,20 @@ Verification VPNaaS is a complex subject, hence this document provides directions for a simple smoke test to verify the service is up and running. -On the network node(s), the ``neutron_vpnaas_agent`` should be up (image naming -and versioning may differ depending on deploy configuration): +In ml2/ovn setups a special neutron_ovn_vpn_agent is running on neutron +node(s). +Version may differ depending on deploy configuration: .. code-block:: console - # docker ps --filter name=neutron_vpnaas_agent + # docker ps --filter name=neutron_ovn_vpn_agent + + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 7f6efad28d30 kolla/neutron-ovn-vpn-agent:18.1.0 "dumb-init --single-…" 7 days ago Up 7 days (healthy) neutron_ovn_vpn_agent + +On ml2/ovs deployments there is no special agent. +The vpnaas code is running inside the neutron_l3_agent container. - CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES - 97d25657d55e operator:5000/kolla/centos-source-neutron-vpnaas-agent:4.0.0 "kolla_start" 44 minutes ago Up 44 minutes neutron_vpnaas_agent .. warning:: @@ -92,21 +104,20 @@ Verify both VPN services are active: .. code-block:: console - # neutron vpn-service-list + # openstack vpn service list - +--------------------------------------+----------+--------------------------------------+--------+ - | id | name | router_id | status | - +--------------------------------------+----------+--------------------------------------+--------+ - | ad941ec4-5f3d-4a30-aae2-1ab3f4347eb1 | vpn_west | 051f7ce3-4301-43cc-bfbd-7ffd59af539e | ACTIVE | - | edce15db-696f-46d8-9bad-03d087f1f682 | vpn_east | 058842e0-1d01-4230-af8d-0ba6d0da8b1f | ACTIVE | - +--------------------------------------+----------+--------------------------------------+--------+ + +--------------------------------------+----------+--------------------------------------+--------+--------+-------+--------+ + | ID | Name | Router | Subnet | Flavor | State | Status | + +--------------------------------------+----------+--------------------------------------+--------+--------+-------+--------+ + | 03f85023-28d9-4f35-a10e-2c8dd3c11b65 | vpn_west | e3603217-fd22-404c-b27e-9285c2a79a17 | None | None | True | ACTIVE | + | 1abdc71a-2eb7-4b2a-8871-eb9d91f39957 | vpn_east | 3485bdd2-4c42-449e-ae9f-d071a8cb9e5c | None | None | True | ACTIVE | + +--------------------------------------+----------+--------------------------------------+--------+--------+-------+--------+ Two VMs can now be booted, one on vpn_east, the other on vpn_west, and encrypted ping packets observed being sent from one to the other. -For more information on this and VPNaaS in Neutron refer to the -:neutron-vpnaas-doc:`Neutron VPNaaS Testing ` -and the `OpenStack wiki `_. +For more information on VPNaaS in Neutron refer to the +`OpenStack docs `_. Trunking ~~~~~~~~ diff --git a/releasenotes/notes/implement-neutron-ovn-vpn-agent-b848fb4c245e415e.yaml b/releasenotes/notes/implement-neutron-ovn-vpn-agent-b848fb4c245e415e.yaml new file mode 100644 index 000000000..dd9ab57a1 --- /dev/null +++ b/releasenotes/notes/implement-neutron-ovn-vpn-agent-b848fb4c245e415e.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add support for deploying neutron_ovn_vpn_agent. + This enables the use of vpnaas in ovn environments. + `__ diff --git a/roles/openstack-clients/defaults/main.yml b/roles/openstack-clients/defaults/main.yml index 6b1e3c6d9..e1f53e279 100644 --- a/roles/openstack-clients/defaults/main.yml +++ b/roles/openstack-clients/defaults/main.yml @@ -14,6 +14,8 @@ openstack_clients_pip_packages: enabled: "{{ scenario == 'ironic' }}" - package: python-magnumclient enabled: "{{ scenario == 'magnum' }}" + - package: python-neutronclient + enabled: "{{ scenario == 'ovn' }}" - package: python-masakariclient enabled: "{{ scenario == 'masakari' }}" - package: python-mistralclient diff --git a/roles/test-ovn-vpnaas/defaults/main.yml b/roles/test-ovn-vpnaas/defaults/main.yml new file mode 100644 index 000000000..e55c4d338 --- /dev/null +++ b/roles/test-ovn-vpnaas/defaults/main.yml @@ -0,0 +1,14 @@ +--- +ovn_vpnaas_openrc: "/etc/kolla/admin-openrc.sh" +ovn_vpnaas_venv: "{{ ansible_env.HOME }}/openstackclient-venv" + +ovn_vpnaas_activate: ". {{ ovn_vpnaas_venv }}/bin/activate &&" + +ovn_vpnaas_west_cidr: "192.168.10.0/24" +ovn_vpnaas_east_cidr: "192.168.20.0/24" +ovn_vpnaas_psk: "secret" + +ovn_vpnaas_consistency_retries: 10 +ovn_vpnaas_consistency_delay: 5 +ovn_vpnaas_tunnel_retries: 20 +ovn_vpnaas_tunnel_delay: 15 diff --git a/roles/test-ovn-vpnaas/tasks/cleanup_site.yml b/roles/test-ovn-vpnaas/tasks/cleanup_site.yml new file mode 100644 index 000000000..403e3020d --- /dev/null +++ b/roles/test-ovn-vpnaas/tasks/cleanup_site.yml @@ -0,0 +1,46 @@ +--- +- name: "VPNaaS | Cleanup | Site: {{ site_to_clean }}" + block: + - name: "VPNaaS | Cleanup | Delete Connection" + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn ipsec site connection delete "conn_{{ site_to_clean }}" || true + environment: "{{ os_auth_env }}" + changed_when: false + + - name: "VPNaaS | Cleanup | Delete VPN Service" + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn service delete "vpn_{{ site_to_clean }}" || true + environment: "{{ os_auth_env }}" + changed_when: false + + - name: "VPNaaS | Cleanup | Delete Endpoint Groups" + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn endpoint group delete "{{ site_to_clean }}-local-epg" || true + openstack vpn endpoint group delete "{{ site_to_clean }}-peer-epg" || true + environment: "{{ os_auth_env }}" + changed_when: false + + - name: "VPNaaS | Cleanup | Remove Subnet from Router" + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack router remove subnet "router_{{ site_to_clean }}" "subnet_{{ site_to_clean }}" || true + environment: "{{ os_auth_env }}" + changed_when: false + + - name: "VPNaaS | Cleanup | Delete Router" + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack router delete "router_{{ site_to_clean }}" || true + environment: "{{ os_auth_env }}" + changed_when: false + + - name: "VPNaaS | Cleanup | Delete Network/Subnet" + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack subnet delete "subnet_{{ site_to_clean }}" || true + openstack network delete "net_{{ site_to_clean }}" || true + environment: "{{ os_auth_env }}" + changed_when: false diff --git a/roles/test-ovn-vpnaas/tasks/init_env.yml b/roles/test-ovn-vpnaas/tasks/init_env.yml new file mode 100644 index 000000000..d448c95fc --- /dev/null +++ b/roles/test-ovn-vpnaas/tasks/init_env.yml @@ -0,0 +1,33 @@ +--- +- name: VPNaaS | Install requirements in venv + ansible.builtin.pip: + name: + - "python-neutronclient" + - "openstacksdk" + virtualenv: "{{ ovn_vpnaas_venv }}" + state: present + +- name: VPNaaS | Load OpenStack credentials from openrc + ansible.builtin.shell: | + set -o pipefail + . {{ ovn_vpnaas_openrc }} + env | grep ^OS_ + args: + executable: /bin/bash + register: openrc_env + changed_when: false + +- name: VPNaaS | Convert OS_* env to dict + ansible.builtin.set_fact: + os_auth_env: >- + {{ + dict( + openrc_env.stdout_lines + | map('split', '=', 1) + | list + ) + }} + +- name: VPNaaS | Add venv to PATH + ansible.builtin.set_fact: + os_auth_env: "{{ os_auth_env | combine({'PATH': ovn_vpnaas_venv + '/bin:' + ansible_facts['env']['PATH']}) }}" diff --git a/roles/test-ovn-vpnaas/tasks/main.yml b/roles/test-ovn-vpnaas/tasks/main.yml new file mode 100644 index 000000000..ee5a718e0 --- /dev/null +++ b/roles/test-ovn-vpnaas/tasks/main.yml @@ -0,0 +1,211 @@ +--- +- name: VPNaaS | Init ENV + ansible.builtin.include_tasks: init_env.yml + +- name: VPNaaS | Check if VPN agents are alive and reporting + ansible.builtin.shell: | + set -o pipefail + {{ ovn_vpnaas_activate }} + openstack network agent list -f value -c Binary -c Alive | grep "vpn-agent" | awk '{print $2}' + args: + executable: /bin/bash + register: vpn_agents_alive + until: + - vpn_agents_alive.stdout != "" + - "'False' not in vpn_agents_alive.stdout" + retries: 10 + delay: 5 + environment: "{{ os_auth_env }}" + changed_when: false + +- name: VPNaaS | Create IKE and IPsec Policies + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn ike policy create ikepolicy --ike-version v2 --auth-algorithm sha256 --encryption-algorithm aes-256 --pfs group14 || true + openstack vpn ipsec policy create ipsecpolicy --auth-algorithm sha256 --encryption-algorithm aes-256 --pfs group14 || true + environment: "{{ os_auth_env }}" + changed_when: false + +- name: VPNaaS | Setup Network Infrastructure + ansible.builtin.include_tasks: setup_site.yml + vars: + site_name: "{{ item.name }}" + site_cidr: "{{ item.cidr }}" + peer_cidr: "{{ item.peer }}" + loop: + - { name: 'west', cidr: "{{ ovn_vpnaas_west_cidr }}", peer: "{{ ovn_vpnaas_east_cidr }}" } + - { name: 'east', cidr: "{{ ovn_vpnaas_east_cidr }}", peer: "{{ ovn_vpnaas_west_cidr }}" } + +- name: VPNaaS | Wait for OVN Agent to settle + ansible.builtin.pause: + minutes: 2 + +- name: VPNaaS | Check if VPN service west exists + ansible.builtin.shell: | + set -o pipefail + {{ ovn_vpnaas_activate }} + openstack vpn service list -f value -c ID -c Name | awk '$2=="vpn_west" {print $1}' | head -n 1 + args: + executable: /bin/bash + environment: "{{ os_auth_env }}" + register: check_vpn_west + changed_when: false + +- name: VPNaaS | Create VPN service | west + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn service create --router "router_west" "vpn_west" + environment: "{{ os_auth_env }}" + when: check_vpn_west.stdout | trim == "" + failed_when: false + changed_when: false + +- name: VPNaaS | Settle database consistency before next service + ansible.builtin.pause: + seconds: 20 + +- name: VPNaaS | Ensure VPN service west exists and get ID + ansible.builtin.shell: | + set -o pipefail + {{ ovn_vpnaas_activate }} + openstack vpn service list -f value -c ID -c Name | awk '$2=="vpn_west" {print $1}' | head -n 1 + args: + executable: /bin/bash + register: vpn_svc_west_out + until: + - vpn_svc_west_out.rc == 0 + - vpn_svc_west_out.stdout | trim != "" + retries: 20 + delay: 10 + environment: "{{ os_auth_env }}" + changed_when: false + +- name: VPNaaS | Check if VPN service east exists + ansible.builtin.shell: | + set -o pipefail + {{ ovn_vpnaas_activate }} + openstack vpn service list -f value -c ID -c Name | awk '$2=="vpn_east" {print $1}' | head -n 1 + args: + executable: /bin/bash + environment: "{{ os_auth_env }}" + register: check_vpn_east + changed_when: false + +- name: VPNaaS | Create VPN service | east + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn service create --router "router_east" "vpn_east" + environment: "{{ os_auth_env }}" + when: check_vpn_east.stdout | trim == "" + failed_when: false + changed_when: false + +- name: VPNaaS | Settle database consistency before next service + ansible.builtin.pause: + seconds: 20 + +- name: VPNaaS | Ensure VPN service east exists in DB + ansible.builtin.shell: | + set -o pipefail + {{ ovn_vpnaas_activate }} + openstack vpn service list -f value -c ID -c Name | awk '$2=="vpn_east" {print $1}' | head -n 1 + args: + executable: /bin/bash + register: vpn_svc_east_out + until: + - vpn_svc_east_out.rc == 0 + - vpn_svc_east_out.stdout | trim != "" + retries: 20 + delay: 10 + environment: "{{ os_auth_env }}" + changed_when: false + +- name: VPNaaS | Create Endpoint Groups + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn endpoint group create --type subnet --value "subnet_{{ item.name }}" "{{ item.name }}-local-epg" || true + openstack vpn endpoint group create --type cidr --value "{{ item.peer }}" "{{ item.name }}-peer-epg" || true + loop: + - { name: 'west', peer: "{{ ovn_vpnaas_east_cidr }}" } + - { name: 'east', peer: "{{ ovn_vpnaas_west_cidr }}" } + environment: "{{ os_auth_env }}" + changed_when: false + +- name: VPNaaS | Get Public IPs + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn service show "{{ item }}" -f value -c ext_v4_ip + register: vpn_ips + until: + - vpn_ips.stdout | trim != "" + - vpn_ips.stdout | trim != "None" + retries: 24 + delay: 10 + loop: + - "{{ vpn_svc_west_out.stdout.strip() }}" + - "{{ vpn_svc_east_out.stdout.strip() }}" + environment: "{{ os_auth_env }}" + changed_when: false + +- name: VPNaaS | Establish Connections + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn ipsec site connection create "conn_east" --vpnservice "{{ vpn_svc_east_out.stdout.strip() }}" --ikepolicy ikepolicy \ + --ipsecpolicy ipsecpolicy --local-endpoint-group east-local-epg --peer-address "{{ vpn_ips.results[0].stdout.strip() }}" \ + --peer-id "{{ vpn_ips.results[0].stdout.strip() }}" --peer-endpoint-group east-peer-epg --psk {{ ovn_vpnaas_psk }} || true + + openstack vpn ipsec site connection create "conn_west" --vpnservice "{{ vpn_svc_west_out.stdout.strip() }}" --ikepolicy ikepolicy \ + --ipsecpolicy ipsecpolicy --local-endpoint-group west-local-epg --peer-address "{{ vpn_ips.results[1].stdout.strip() }}" \ + --peer-id "{{ vpn_ips.results[1].stdout.strip() }}" --peer-endpoint-group west-peer-epg --psk {{ ovn_vpnaas_psk }} || true + environment: "{{ os_auth_env }}" + changed_when: false + +- name: VPNaaS | Wait for Tunnel to be ACTIVE + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn ipsec site connection show "conn_{{ item }}" -f value -c status + register: tunnel_status + until: tunnel_status.stdout == "ACTIVE" + retries: "{{ ovn_vpnaas_tunnel_retries }}" + delay: "{{ ovn_vpnaas_tunnel_delay }}" + loop: ["west", "east"] + environment: "{{ os_auth_env }}" + changed_when: false + +- name: VPNaaS | SHOW TUNNEL STATUS RESULT + ansible.builtin.debug: + msg: + - "************************************************************" + - "CONNECTION: conn_{{ item.item }}" + - "STATUS: {{ item.stdout }}" + - "RESULT: TUNNEL ESTABLISHED SUCCESSFULLY!" + - "************************************************************" + loop: "{{ tunnel_status.results }}" + loop_control: + label: "{{ item.item }}" + +- name: VPNaaS | Show Full Connection Details + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn ipsec site connection list + environment: "{{ os_auth_env }}" + register: final_list + changed_when: false + +- name: VPNaaS | Print Connection List Table + ansible.builtin.debug: + var: final_list.stdout_lines + +- name: VPNaaS | Final Cleanup + ansible.builtin.include_tasks: cleanup_site.yml + loop: ["west", "east"] + loop_control: + loop_var: site_to_clean + +- name: VPNaaS | Cleanup | Global Policies + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack vpn ipsec policy delete ipsecpolicy || true + openstack vpn ike policy delete ikepolicy || true + environment: "{{ os_auth_env }}" + changed_when: false diff --git a/roles/test-ovn-vpnaas/tasks/setup_site.yml b/roles/test-ovn-vpnaas/tasks/setup_site.yml new file mode 100644 index 000000000..f683dc596 --- /dev/null +++ b/roles/test-ovn-vpnaas/tasks/setup_site.yml @@ -0,0 +1,76 @@ +--- +- name: VPNaaS | Create network | {{ site_name }} + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack network create "net_{{ site_name }}" + environment: "{{ os_auth_env }}" + register: net_create + failed_when: + - net_create.rc != 0 + - "'already exists' not in net_create.stderr" + retries: "{{ ovn_vpnaas_consistency_retries }}" + delay: "{{ ovn_vpnaas_consistency_delay }}" + until: net_create.rc == 0 or 'already exists' in net_create.stderr + changed_when: false + +- name: VPNaaS | Create subnet | {{ site_name }} + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack subnet create --network "net_{{ site_name }}" --subnet-range "{{ site_cidr }}" "subnet_{{ site_name }}" + environment: "{{ os_auth_env }}" + register: subnet_create + failed_when: + - subnet_create.rc != 0 + - "'already exists' not in subnet_create.stderr" + retries: "{{ ovn_vpnaas_consistency_retries }}" + delay: "{{ ovn_vpnaas_consistency_delay }}" + until: subnet_create.rc == 0 or 'already exists' in subnet_create.stderr + changed_when: false + +- name: VPNaaS | Create router | {{ site_name }} + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack router create --external-gateway public1 "router_{{ site_name }}" + environment: "{{ os_auth_env }}" + failed_when: false + changed_when: false + +- name: VPNaaS | Debug | List all routers + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack router list + environment: "{{ os_auth_env }}" + register: all_routers_debug + changed_when: false + +- name: VPNaaS | Debug | Show router list output + ansible.builtin.debug: + var: all_routers_debug.stdout_lines + +- name: VPNaaS | Get router ID | {{ site_name }} + ansible.builtin.shell: | + set -o pipefail + {{ ovn_vpnaas_activate }} + openstack router list --name "router_{{ site_name }}" -f value -c ID | head -n 1 + args: + executable: /bin/bash + register: current_router_id + until: current_router_id.stdout != "" + retries: 5 + delay: 10 + environment: "{{ os_auth_env }}" + changed_when: false + +- name: VPNaaS | Attach subnet to router | {{ site_name }} + ansible.builtin.shell: | + {{ ovn_vpnaas_activate }} + openstack router add subnet "{{ current_router_id.stdout.strip() }}" "subnet_{{ site_name }}" + environment: "{{ os_auth_env }}" + register: router_add_result + failed_when: + - router_add_result.rc != 0 + - "'already has an interface' not in router_add_result.stderr" + retries: "{{ ovn_vpnaas_consistency_retries }}" + delay: "{{ ovn_vpnaas_consistency_delay }}" + until: router_add_result.rc == 0 or 'already has an interface' in router_add_result.stderr + changed_when: false diff --git a/tests/run.yml b/tests/run.yml index 78d34d8eb..775eb2630 100644 --- a/tests/run.yml +++ b/tests/run.yml @@ -429,6 +429,14 @@ SCENARIO: "{{ scenario }}" when: openstack_core_tested or scenario in ['ironic', 'magnum', 'nfv', 'zun', 'octavia'] + - name: Test OVN VPNaaS + import_role: + name: test-ovn-vpnaas + when: + - scenario == 'ovn' + # FIXME(neutron): Enable when https://bugs.launchpad.net/neutron/+bug/2146308 is fixed + - not (ansible_facts.os_family == 'RedHat' and ansible_facts.distribution_major_version == '10') + - name: Run test-ovn.sh script script: cmd: test-ovn.sh diff --git a/tests/templates/globals-default.j2 b/tests/templates/globals-default.j2 index 313e7d5cf..d313d97e7 100644 --- a/tests/templates/globals-default.j2 +++ b/tests/templates/globals-default.j2 @@ -195,6 +195,7 @@ neutron_plugin_agent: "ovn" neutron_ovn_distributed_fip: "yes" neutron_enable_ovn_agent: "yes" ovn_sb_db_relay_count: "3" +enable_neutron_vpnaas: "yes" enable_octavia: "yes" octavia_provider_drivers: "ovn:OVN provider" octavia_provider_agents: "ovn" diff --git a/tests/templates/inventory.j2 b/tests/templates/inventory.j2 index 9a7787b31..a5001a55a 100644 --- a/tests/templates/inventory.j2 +++ b/tests/templates/inventory.j2 @@ -347,6 +347,9 @@ control compute network +[neutron-ovn-vpn-agent:children] +neutron + # Cinder [cinder-api:children] cinder diff --git a/tools/init-vpn b/tools/init-vpn index bdcc85c4b..86dda9130 100755 --- a/tools/init-vpn +++ b/tools/init-vpn @@ -1,66 +1,129 @@ #!/usr/bin/env bash - # Script originally copied from https://wiki.openstack.org/wiki/Neutron/VPNaaS/HowToInstall -# Test for credentials set -if [[ "${OS_USERNAME}" == "" ]]; then - echo "No Keystone credentials specified. Try running source openrc" - exit -fi - -EXT_NW_ID=`neutron net-list | awk '/public/{print $2}'` -WEST_SUBNET='192.168.1.0/24' -EAST_SUBNET='192.168.2.0/24' +WEST_SUBNET='192.168.10.0/24' +EAST_SUBNET='192.168.20.0/24' function setup_site(){ local site_name=$1 local cidr=$2 - neutron net-create net_$site_name - neutron subnet-create --name subnet_$site_name net_$site_name $2 - neutron router-create router_$site_name - neutron router-interface-add router_$site_name subnet_$site_name - neutron router-gateway-set router_$site_name $EXT_NW_ID - neutron vpn-service-create --name vpn_$site_name router_$site_name subnet_$site_name + echo ">>> Creating network and subnet for site '$site_name'..." + openstack network create net_$site_name + openstack subnet create --network net_$site_name --subnet-range $cidr subnet_$site_name + + echo ">>> Creating router '$site_name' with external gateway 'public1'..." + openstack router create --external-gateway public1 router_$site_name + openstack router add subnet router_$site_name subnet_$site_name + + echo ">>> Creating VPN service for site '$site_name'..." + openstack vpn service create vpn_$site_name --router router_$site_name + + echo ">>> Creating local endpoint group for site '$site_name'..." + openstack vpn endpoint group create ${site_name}-local-epg --type subnet --value subnet_$site_name } function get_external_ip(){ - local router_id=`neutron router-show $1 | awk '/ id /{print $4}'` - echo `neutron router-list | grep '$router_id' | awk -F '"' '{print $16}'` + openstack vpn service show -f value -c ext_v4_ip $1 } function clean_site(){ local site_name=$1 - neutron ipsec-site-connection-delete conn_$site_name - neutron vpn-service-list | awk '/vpn_'$site_name'/{print "neutron vpn-service-delete " $2}' | -bash - neutron router-gateway-clear router_$site_name - neutron router-interface-delete router_$site_name subnet_$site_name - neutron router-list | awk '/router_'$site_name'/{print "neutron router-delete " $2}' | bash - neutron subnet-list | awk '/subnet_'$site_name'/{print "neutron subnet-delete " $2}' | bash - neutron net-list | awk '/net_'$site_name'/{print "neutron net-delete " $2}' | bash + echo ">>> Cleaning configuration for site '$site_name'..." + openstack vpn ipsec site connection delete conn_$site_name || true + openstack vpn service delete vpn_$site_name || true + openstack vpn endpoint group delete ${site_name}-local-epg || true + openstack router remove subnet router_$site_name subnet_$site_name || true + openstack router unset --external-gateway router_$site_name || true + openstack router delete router_$site_name || true + openstack subnet delete subnet_$site_name || true + openstack network delete net_$site_name || true } function setup(){ - neutron vpn-ikepolicy-create ikepolicy1 - neutron vpn-ipsecpolicy-create ipsecpolicy1 + echo ">>> Creating IKE policy..." + openstack vpn ike policy create ikepolicy --ike-version v2 --auth-algorithm sha256 --encryption-algorithm aes-256 --pfs group14 + + echo ">>> Creating IPSec policy..." + openstack vpn ipsec policy create ipsecpolicy --auth-algorithm sha256 --encryption-algorithm aes-256 --pfs group14 + + echo ">>> Configuring WEST site..." setup_site west $WEST_SUBNET - WEST_IP=$(get_external_ip router_west) + echo ">>> Creating peer endpoint group for WEST..." + openstack vpn endpoint group create west-peer-epg --type cidr --value $EAST_SUBNET + WEST_IP=$(get_external_ip vpn_west) + echo ">>> VPN WEST external IP: $WEST_IP" + + echo ">>> Configuring EAST site..." setup_site east $EAST_SUBNET - EAST_IP=$(get_external_ip router_east) - neutron ipsec-site-connection-create --name conn_east --vpnservice-id vpn_east --ikepolicy-id \ -ikepolicy1 --ipsecpolicy-id ipsecpolicy1 --peer-address $WEST_IP --peer-id $WEST_IP --peer-cidr \ -$WEST_SUBNET --psk secret - neutron ipsec-site-connection-create --name conn_west --vpnservice-id vpn_west --ikepolicy-id \ -ikepolicy1 --ipsecpolicy-id ipsecpolicy1 --peer-address $EAST_IP --peer-id $EAST_IP --peer-cidr \ -$EAST_SUBNET --psk secret + echo ">>> Creating peer endpoint group for EAST..." + openstack vpn endpoint group create east-peer-epg --type cidr --value $WEST_SUBNET + EAST_IP=$(get_external_ip vpn_east) + echo ">>> VPN EAST external IP: $EAST_IP" + + echo ">>> Creating VPN connection 'conn_east'..." + openstack vpn ipsec site connection create conn_east --vpnservice vpn_east --ikepolicy ikepolicy \ + --ipsecpolicy ipsecpolicy --local-endpoint-group east-local-epg --peer-address $WEST_IP \ + --peer-id $WEST_IP --peer-endpoint-group east-peer-epg --psk secret + + echo ">>> Creating VPN connection 'conn_west'..." + openstack vpn ipsec site connection create conn_west --vpnservice vpn_west --ikepolicy ikepolicy \ + --ipsecpolicy ipsecpolicy --local-endpoint-group west-local-epg --peer-address $EAST_IP \ + --peer-id $EAST_IP --peer-endpoint-group west-peer-epg --psk secret + + echo ">>> Waiting for VPN connections to become ACTIVE..." + wait_for_vpn_active conn_east + wait_for_vpn_active conn_west + + echo ">>> All VPN connections are ACTIVE!" +} + +function wait_for_vpn_active(){ + local conn_name=$1 + local timeout=300 + local interval=10 + local elapsed=0 + + echo ">>> Checking VPN status for $conn_name every $interval seconds (timeout: $timeout seconds)..." + + while true; do + STATUS=$(openstack vpn ipsec site connection show $conn_name -f value -c status) + if [ "$STATUS" == "ACTIVE" ]; then + echo ">>> VPN connection $conn_name is ACTIVE" + break + fi + + if [ $elapsed -ge $timeout ]; then + echo ">>> Timeout waiting for VPN connection $conn_name to become ACTIVE" + exit 1 + fi + + sleep $interval + elapsed=$((elapsed + interval)) + done } function cleanup(){ + echo ">>> Cleaning all VPN configurations..." clean_site west clean_site east - neutron vpn-ikepolicy-delete ikepolicy1 - neutron vpn-ipsecpolicy-delete ipsecpolicy1 + openstack vpn endpoint group delete west-peer-epg || true + openstack vpn endpoint group delete east-peer-epg || true + openstack vpn ike policy delete ikepolicy || true + openstack vpn ipsec policy delete ipsecpolicy || true +} + +function check() { + if [ -z ${OS_USERNAME} ]; then + echo ">>> Keystone credentials not found. Try: source /etc/kolla/admin-openrc.sh" + exit 1 + fi } +check cleanup setup + +echo ">>> Displaying current VPN connections:" +openstack vpn ipsec site connection list + +echo ">>> VPN setup completed successfully." -- cgit v1.2.3