diff --git a/elements/haproxy-octavia/post-install.d/20-haproxy-tune-kernel b/elements/haproxy-octavia/post-install.d/20-haproxy-tune-kernel index 6474266080..d1b5bd63e6 100755 --- a/elements/haproxy-octavia/post-install.d/20-haproxy-tune-kernel +++ b/elements/haproxy-octavia/post-install.d/20-haproxy-tune-kernel @@ -23,6 +23,7 @@ sysctl-write-value net.ipv4.netfilter.ip_conntrack_tcp_timeout_fin_wait 5 || tru sysctl-write-value net.ipv4.tcp_fin_timeout 5 sysctl-write-value net.ipv4.ip_nonlocal_bind 1 +sysctl-write-value net.ipv6.ip_nonlocal_bind 1 sysctl-write-value net.ipv4.tcp_rmem "16384 65536 524288" sysctl-write-value net.ipv4.tcp_wmem "16384 349520 699040" sysctl-write-value net.ipv4.ip_local_port_range "1025 65534" diff --git a/elements/keepalived-octavia/post-install.d/11-ip6-tables b/elements/keepalived-octavia/post-install.d/11-ip6-tables new file mode 100755 index 0000000000..01b5af50fc --- /dev/null +++ b/elements/keepalived-octavia/post-install.d/11-ip6-tables @@ -0,0 +1,11 @@ +#!/bin/bash +# keepalived older than 1.3 will segfault when using IPv6 VIPs if the +# ip6_tables module is not loaded. Make sure it is loaded on releases we +# know have the older version. + +set -eu +set -o xtrace + +if [ "$DISTRO_NAME" == "ubuntu" ] && { [ "$DIB_RELEASE" == "trusty" ] || [ "$DIB_RELEASE" == "xenial" ]; }; then + echo ip6_tables > /etc/modules-load.d/ip6_tables.conf +fi diff --git a/octavia/amphorae/backends/agent/agent_jinja_cfg.py b/octavia/amphorae/backends/agent/agent_jinja_cfg.py index 5a151bee93..8cd22dbba3 100644 --- a/octavia/amphorae/backends/agent/agent_jinja_cfg.py +++ b/octavia/amphorae/backends/agent/agent_jinja_cfg.py @@ -59,4 +59,5 @@ class AgentJinjaTemplater(object): 'heartbeat_key': CONF.health_manager.heartbeat_key, 'use_upstart': CONF.haproxy_amphora.use_upstart, 'respawn_count': CONF.haproxy_amphora.respawn_count, - 'respawn_interval': CONF.haproxy_amphora.respawn_interval}) + 'respawn_interval': CONF.haproxy_amphora.respawn_interval, + 'topology': CONF.controller_worker.loadbalancer_topology}) diff --git a/octavia/amphorae/backends/agent/api_server/keepalived.py b/octavia/amphorae/backends/agent/api_server/keepalived.py index e0400e7a14..59dd42e105 100644 --- a/octavia/amphorae/backends/agent/api_server/keepalived.py +++ b/octavia/amphorae/backends/agent/api_server/keepalived.py @@ -64,6 +64,11 @@ class Keepalived(object): if init_system == consts.INIT_SYSTEMD: template = SYSTEMD_TEMPLATE init_enable_cmd = "systemctl enable octavia-keepalived" + + # Render and install the network namespace systemd service + util.install_netns_systemd_service() + util.run_systemctl_command( + consts.ENABLE, consts.AMP_NETNS_SVC_PREFIX) elif init_system == consts.INIT_UPSTART: template = UPSTART_TEMPLATE elif init_system == consts.INIT_SYSVINIT: @@ -86,7 +91,8 @@ class Keepalived(object): keepalived_cmd=consts.KEEPALIVED_CMD, keepalived_cfg=util.keepalived_cfg_path(), keepalived_log=util.keepalived_log_path(), - amphora_nsname=consts.AMPHORA_NAMESPACE + amphora_nsname=consts.AMPHORA_NAMESPACE, + amphora_netns=consts.AMP_NETNS_SVC_PREFIX ) text_file.write(text) diff --git a/octavia/amphorae/backends/agent/api_server/osutils.py b/octavia/amphorae/backends/agent/api_server/osutils.py index 71272a0cbb..2b4d19117b 100644 --- a/octavia/amphorae/backends/agent/api_server/osutils.py +++ b/octavia/amphorae/backends/agent/api_server/osutils.py @@ -100,6 +100,7 @@ class BaseOS(object): with os.fdopen(os.open(interface_file_path, flags, mode), 'w') as text_file: text = template_vip.render( + consts=consts, interface=primary_interface, vip=vip, vip_ipv6=ip.version is 6, @@ -112,6 +113,7 @@ class BaseOS(object): vrrp_ip=vrrp_ip, vrrp_ipv6=vrrp_version is 6, host_routes=render_host_routes, + topology=CONF.controller_worker.loadbalancer_topology, ) text_file.write(text) @@ -207,9 +209,11 @@ class BaseOS(object): def bring_interfaces_up(self, ip, primary_interface, secondary_interface): self._bring_if_down(primary_interface) - self._bring_if_down(secondary_interface) + if secondary_interface: + self._bring_if_down(secondary_interface) self._bring_if_up(primary_interface, 'VIP') - self._bring_if_up(secondary_interface, 'VIP') + if secondary_interface: + self._bring_if_up(secondary_interface, 'VIP') def has_ifup_all(self): return True @@ -373,7 +377,10 @@ class RH(BaseOS): netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes, template_vip) - if ip.version == 4: + # keepalived will handle the VIP if we are on active/standby + if (ip.version == 4 and + CONF.controller_worker.loadbalancer_topology == + consts.TOPOLOGY_SINGLE): # Create an IPv4 alias interface, needed in RH based flavors alias_interface_file_path = self.get_alias_network_interface_file( primary_interface) @@ -391,13 +398,16 @@ class RH(BaseOS): routes_interface_file_path, primary_interface, render_host_routes, template_routes, gateway, vip, netmask) - route_rules_interface_file_path = ( - self.get_route_rules_interface_file(primary_interface)) - template_rules = j2_env.get_template(self.RULE_ETH_X_CONF) + # keepalived will handle the rule(s) if we are on actvie/standby + if (CONF.controller_worker.loadbalancer_topology == + consts.TOPOLOGY_SINGLE): + route_rules_interface_file_path = ( + self.get_route_rules_interface_file(primary_interface)) + template_rules = j2_env.get_template(self.RULE_ETH_X_CONF) - self.write_static_routes_interface_file( - route_rules_interface_file_path, primary_interface, - render_host_routes, template_rules, gateway, vip, netmask) + self.write_static_routes_interface_file( + route_rules_interface_file_path, primary_interface, + render_host_routes, template_rules, gateway, vip, netmask) def write_static_routes_interface_file(self, interface_file_path, interface, host_routes, @@ -416,11 +426,13 @@ class RH(BaseOS): with os.fdopen(os.open(interface_file_path, flags, mode), 'w') as text_file: text = template_routes.render( + consts=consts, interface=interface, host_routes=host_routes, gateway=gateway, network=utils.ip_netmask_to_cidr(vip, netmask), vip=vip, + topology=CONF.controller_worker.loadbalancer_topology, ) text_file.write(text) diff --git a/octavia/amphorae/backends/agent/api_server/plug.py b/octavia/amphorae/backends/agent/api_server/plug.py index df2500cade..fbc8ab267f 100644 --- a/octavia/amphorae/backends/agent/api_server/plug.py +++ b/octavia/amphorae/backends/agent/api_server/plug.py @@ -20,7 +20,6 @@ import socket import stat import subprocess -import jinja2 import netifaces from oslo_config import cfg import pyroute2 @@ -38,11 +37,6 @@ ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2' LOG = logging.getLogger(__name__) -j2_env = jinja2.Environment(autoescape=True, loader=jinja2.FileSystemLoader( - os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES)) -template_port = j2_env.get_template(ETH_X_PORT_CONF) -template_vip = j2_env.get_template(ETH_X_VIP_CONF) - class Plug(object): def __init__(self, osutils): @@ -137,6 +131,10 @@ class Plug(object): ipr.link('set', index=idx, net_ns_fd=consts.AMPHORA_NAMESPACE, IFLA_IFNAME=primary_interface) + # In an ha amphora, keepalived should bring the VIP interface up + if (CONF.controller_worker.loadbalancer_topology == + consts.TOPOLOGY_ACTIVE_STANDBY): + secondary_interface = None # bring interfaces up self._osutils.bring_interfaces_up( ip, primary_interface, secondary_interface) diff --git a/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 index 6487814f8d..47c4449cb6 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 @@ -15,7 +15,11 @@ # under the License. #} # Generated by Octavia agent +{%- if topology == consts.TOPOLOGY_SINGLE %} auto {{ interface }} {{ interface }}:0 +{%- else %} +auto {{ interface }} +{%- endif %} {%- if vrrp_ip %} iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static address {{ vrrp_ip }} @@ -42,20 +46,34 @@ down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }} {%- endif %} +{%- if topology == consts.TOPOLOGY_SINGLE %} iface {{ interface }}:0 inet{{ '6' if vip_ipv6 }} static address {{ vip }} broadcast {{ broadcast }} netmask {{ netmask }} +{%- endif %} + # Add a source routing table to allow members to access the VIP {%- if gateway %} -post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add {{ network }} dev {{ interface }} src {{ vip }} scope link table 1 + post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add default via {{ gateway }} dev {{ interface }} onlink table 1 post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del default via {{ gateway }} dev {{ interface }} onlink table 1 + +{# Keepalived will insert and remove this route in active/standby #} +{%- if topology == consts.TOPOLOGY_SINGLE %} +post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add {{ network }} dev {{ interface }} src {{ vip }} scope link table 1 post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del {{ network }} dev {{ interface }} src {{ vip }} scope link table 1 {%- endif %} + +{%- endif %} + {%- for hr in host_routes %} post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add {{ hr.network }} via {{ hr.gw }} dev {{ interface }} onlink table 1 post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del {{ hr.network }} via {{ hr.gw }} dev {{ interface }} onlink table 1 {%- endfor %} -post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}rule add from {{ vip }}/32 table 1 priority 100 -post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}rule del from {{ vip }}/32 table 1 priority 100 + +{# Keepalived will insert and remove this rule in active/standby #} +{%- if topology == consts.TOPOLOGY_SINGLE %} +post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}rule add from {{ vip }}/{{ '128' if vip_ipv6 else '32' }} table 1 priority 100 +post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}rule del from {{ vip }}/{{ '128' if vip_ipv6 else '32' }} table 1 priority 100 +{%- endif %} diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 index 140bed2d0c..3ce6777c1d 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 @@ -19,7 +19,9 @@ {%- endfor %} # Add a source routing table to allow members to access the VIP {%- if gateway %} +{%- if topology == consts.TOPOLOGY_SINGLE %} {{ network }} dev {{ interface }} src {{ vip }} scope link table 1 +{%- endif %} default table 1 via {{ gateway }} dev {{ interface }} {%- endif %} {%- for hr in host_routes %} diff --git a/octavia/amphorae/backends/agent/api_server/util.py b/octavia/amphorae/backends/agent/api_server/util.py index 422dadd68d..9687f62d83 100644 --- a/octavia/amphorae/backends/agent/api_server/util.py +++ b/octavia/amphorae/backends/agent/api_server/util.py @@ -14,13 +14,18 @@ import os +import stat import subprocess +import jinja2 from oslo_config import cfg +from oslo_log import log as logging +from octavia.amphorae.backends.agent.api_server import osutils from octavia.common import constants as consts CONF = cfg.CONF +LOG = logging.getLogger(__name__) class UnknownInitError(Exception): @@ -141,3 +146,39 @@ def get_os_init_system(): else: return consts.INIT_SYSVINIT return consts.INIT_UNKOWN + + +def install_netns_systemd_service(): + os_utils = osutils.BaseOS.get_os_util() + + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + # mode 00644 + mode = (stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) + + # TODO(bcafarel): implement this for other init systems + # netns handling depends on a separate unit file + netns_path = os.path.join(consts.SYSTEMD_DIR, + consts.AMP_NETNS_SVC_PREFIX + '.service') + + jinja_env = jinja2.Environment( + autoescape=True, loader=jinja2.FileSystemLoader(os.path.dirname( + os.path.realpath(__file__) + ) + consts.AGENT_API_TEMPLATES)) + + if not os.path.exists(netns_path): + with os.fdopen(os.open(netns_path, flags, mode), 'w') as text_file: + text = jinja_env.get_template( + consts.AMP_NETNS_SVC_PREFIX + '.systemd.j2').render( + amphora_nsname=consts.AMPHORA_NAMESPACE, + HasIFUPAll=os_utils.has_ifup_all()) + text_file.write(text) + + +def run_systemctl_command(command, service): + cmd = "systemctl {cmd} {srvc}".format(cmd=command, srvc=service) + try: + subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + LOG.error("Failed to %(cmd)s %(srvc)s service: " + "%(err)s %(out)s", {'cmd': command, 'srvc': service, + 'err': e, 'out': e.output}) diff --git a/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template b/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template index cca80bd28a..9bc1d2d106 100644 --- a/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template +++ b/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template @@ -42,3 +42,6 @@ agent_server_network_file = {{ agent_server_network_file }} {% endif -%} agent_request_read_timeout = {{ agent_request_read_timeout }} amphora_id = {{ amphora_id }} + +[controller_worker] +loadbalancer_topology = {{ topology }} diff --git a/octavia/amphorae/drivers/keepalived/jinja/jinja_cfg.py b/octavia/amphorae/drivers/keepalived/jinja/jinja_cfg.py index 31d6ed5f3c..7175a76d43 100644 --- a/octavia/amphorae/drivers/keepalived/jinja/jinja_cfg.py +++ b/octavia/amphorae/drivers/keepalived/jinja/jinja_cfg.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import ipaddress import os import jinja2 @@ -53,11 +54,12 @@ class KeepalivedJinjaTemplater(object): lstrip_blocks=True) return self._jinja_env.get_template(os.path.basename(template_file)) - def build_keepalived_config(self, loadbalancer, amphora): + def build_keepalived_config(self, loadbalancer, amphora, vip_cidr): """Renders the loadblanacer keepalived configuration for Active/Standby :param loadbalancer: A lodabalancer object :param amp: An amphora object + :param vip_cidr: The VIP subnet cidr """ # Note on keepalived configuration: The current base configuration # enforced Master election whenever a high priority VRRP instance @@ -67,6 +69,22 @@ class KeepalivedJinjaTemplater(object): # several backend services. To disable the fallback behavior, we need # to add the "nopreempt" flag in the backup instance section. peers_ips = [] + + # Validate the VIP address and see if it is IPv6 + vip = loadbalancer.vip.ip_address + vip_addr = ipaddress.ip_address( + vip if isinstance(vip, six.text_type) else six.u(vip)) + vip_ipv6 = True if vip_addr.version == 6 else False + + # Normalize and validate the VIP subnet CIDR + vip_network_cidr = None + vip_cidr = (vip_cidr if isinstance(vip_cidr, six.text_type) else + six.u(vip_cidr)) + if vip_ipv6: + vip_network_cidr = ipaddress.IPv6Network(vip_cidr).with_prefixlen + else: + vip_network_cidr = ipaddress.IPv4Network(vip_cidr).with_prefixlen + for amp in six.moves.filter( lambda amp: amp.status == constants.AMPHORA_ALLOCATED, loadbalancer.amphorae): @@ -86,12 +104,14 @@ class KeepalivedJinjaTemplater(object): 'vrrp_auth_pass': loadbalancer.vrrp_group.vrrp_auth_pass, 'amp_vrrp_ip': amphora.vrrp_ip, 'peers_vrrp_ips': peers_ips, - 'vip_ip_address': loadbalancer.vip.ip_address, + 'vip_ip_address': vip, 'advert_int': loadbalancer.vrrp_group.advert_int, 'check_script_path': util.keepalived_check_script_path(), 'vrrp_check_interval': CONF.keepalived_vrrp.vrrp_check_interval, 'vrrp_fail_count': CONF.keepalived_vrrp.vrrp_fail_count, 'vrrp_success_count': - CONF.keepalived_vrrp.vrrp_success_count}, + CONF.keepalived_vrrp.vrrp_success_count, + 'vip_network_cidr': vip_network_cidr, + 'vip_ipv6': vip_ipv6}, constants=constants) diff --git a/octavia/amphorae/drivers/keepalived/jinja/templates/keepalived_base.template b/octavia/amphorae/drivers/keepalived/jinja/templates/keepalived_base.template index ffce44fede..b7fd799841 100644 --- a/octavia/amphorae/drivers/keepalived/jinja/templates/keepalived_base.template +++ b/octavia/amphorae/drivers/keepalived/jinja/templates/keepalived_base.template @@ -26,6 +26,7 @@ vrrp_instance {{ vrrp_group_name }} { virtual_router_id {{ amp_vrrp_id }} priority {{ amp_priority }} nopreempt + accept garp_master_refresh {{ vrrp_garp_refresh }} garp_master_refresh_repeat {{ vrrp_garp_refresh_repeat }} advert_int {{ advert_int }} @@ -44,8 +45,16 @@ vrrp_instance {{ vrrp_group_name }} { virtual_ipaddress { {{ vip_ip_address }} } + + virtual_routes { + {{ vip_network_cidr }} dev {{ amp_intf }} src {{ vip_ip_address }} scope link table 1 + } + + virtual_rules { + from {{ vip_ip_address }}/{{ '128' if vip_ipv6 else '32' }} table 1 priority 100 + } + track_script { check_script } } - diff --git a/octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py b/octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py index a337b9173f..42794a70d3 100644 --- a/octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py +++ b/octavia/amphorae/drivers/keepalived/vrrp_rest_driver.py @@ -30,10 +30,11 @@ class KeepalivedAmphoraDriverMixin(driver_base.VRRPDriverMixin): # The Mixed class must define a self.client object for the # AmphoraApiClient - def update_vrrp_conf(self, loadbalancer): + def update_vrrp_conf(self, loadbalancer, amphorae_network_config): """Update amphorae of the loadbalancer with a new VRRP configuration :param loadbalancer: loadbalancer object + :param amphorae_network_config: amphorae network configurations """ templater = jinja_cfg.KeepalivedJinjaTemplater() @@ -43,8 +44,13 @@ class KeepalivedAmphoraDriverMixin(driver_base.VRRPDriverMixin): for amp in six.moves.filter( lambda amp: amp.status == constants.AMPHORA_ALLOCATED, loadbalancer.amphorae): + + # Get the VIP subnet prefix for the amphora + vip_cidr = amphorae_network_config[amp.id].vip_subnet.cidr + # Generate Keepalived configuration from loadbalancer object - config = templater.build_keepalived_config(loadbalancer, amp) + config = templater.build_keepalived_config( + loadbalancer, amp, vip_cidr) self.client.upload_vrrp_config(amp, config) def stop_vrrp_service(self, loadbalancer): diff --git a/octavia/common/constants.py b/octavia/common/constants.py index 30e408e07a..2a5b08a077 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -511,3 +511,10 @@ SUPPORTED_PROVIDERS = OCTAVIA, # FLAVORS # TODO(johnsom) When flavors are implemented, this should be removed. SUPPORTED_FLAVORS = () + +# systemctl commands +DISABLE = 'disable' +ENABLE = 'enable' + +# systemd amphora netns service prefix +AMP_NETNS_SVC_PREFIX = 'amphora-netns' diff --git a/octavia/controller/worker/flows/amphora_flows.py b/octavia/controller/worker/flows/amphora_flows.py index 6a3d9056ed..bd37a8d4cb 100644 --- a/octavia/controller/worker/flows/amphora_flows.py +++ b/octavia/controller/worker/flows/amphora_flows.py @@ -480,7 +480,8 @@ class AmphoraFlows(object): provides=constants.LOADBALANCER)) vrrp_subflow.add(amphora_driver_tasks.AmphoraVRRPUpdate( name=sf_name + '-' + constants.AMP_VRRP_UPDATE, - requires=constants.LOADBALANCER)) + requires=(constants.LOADBALANCER, + constants.AMPHORAE_NETWORK_CONFIG))) vrrp_subflow.add(amphora_driver_tasks.AmphoraVRRPStart( name=sf_name + '-' + constants.AMP_VRRP_START, requires=constants.LOADBALANCER)) diff --git a/octavia/controller/worker/tasks/amphora_driver_tasks.py b/octavia/controller/worker/tasks/amphora_driver_tasks.py index a2f2d95b0b..eaae0be0b6 100644 --- a/octavia/controller/worker/tasks/amphora_driver_tasks.py +++ b/octavia/controller/worker/tasks/amphora_driver_tasks.py @@ -331,9 +331,10 @@ class AmphoraUpdateVRRPInterface(BaseAmphoraTask): class AmphoraVRRPUpdate(BaseAmphoraTask): """Task to update the VRRP configuration of the loadbalancer amphorae.""" - def execute(self, loadbalancer): + def execute(self, loadbalancer, amphorae_network_config): """Execute update_vrrp_conf.""" - self.amphora_driver.update_vrrp_conf(loadbalancer) + self.amphora_driver.update_vrrp_conf(loadbalancer, + amphorae_network_config) LOG.debug("Uploaded VRRP configuration of loadbalancer %s amphorae", loadbalancer.id) diff --git a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py index be706eb240..bf9af197f1 100644 --- a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +++ b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py @@ -45,6 +45,10 @@ class TestServerTestCase(base.TestCase): def setUp(self): super(TestServerTestCase, self).setUp() + self.conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF)) + self.conf.config(group="haproxy_amphora", base_path='/var/lib/octavia') + self.conf.config(group="controller_worker", + loadbalancer_topology=consts.TOPOLOGY_SINGLE) with mock.patch('platform.linux_distribution', return_value=['Ubuntu', 'Foo', 'Bar']): self.ubuntu_test_server = server.Server() @@ -55,9 +59,6 @@ class TestServerTestCase(base.TestCase): self.centos_test_server = server.Server() self.centos_app = self.centos_test_server.app.test_client() - self.conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF)) - self.conf.config(group="haproxy_amphora", base_path='/var/lib/octavia') - @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSTEMD) def test_ubuntu_haproxy_systemd(self, mock_init_system): @@ -1691,19 +1692,18 @@ class TestServerTestCase(base.TestCase): 'dev {netns_int}\n' 'down route del -host 203.0.115.1/32 gw 203.0.113.5 ' 'dev {netns_int}\n' - '\n' 'iface {netns_int}:0 inet static\n' 'address 203.0.113.2\n' 'broadcast 203.0.113.255\n' - 'netmask 255.255.255.0\n' + 'netmask 255.255.255.0\n\n' '# Add a source routing table to allow members to ' - 'access the VIP\n' - 'post-up /sbin/ip route add 203.0.113.0/24 ' - 'dev eth1 src 203.0.113.2 scope link table 1\n' + 'access the VIP\n\n' 'post-up /sbin/ip route add default via 203.0.113.1 ' 'dev eth1 onlink table 1\n' 'post-down /sbin/ip route del default via 203.0.113.1 ' - 'dev eth1 onlink table 1\n' + 'dev eth1 onlink table 1\n\n\n' + 'post-up /sbin/ip route add 203.0.113.0/24 ' + 'dev eth1 src 203.0.113.2 scope link table 1\n' 'post-down /sbin/ip route del 203.0.113.0/24 ' 'dev eth1 src 203.0.113.2 scope link table 1\n' 'post-up /sbin/ip route add 203.0.114.0/24 ' @@ -1713,7 +1713,7 @@ class TestServerTestCase(base.TestCase): 'post-up /sbin/ip route add 203.0.115.1/32 ' 'via 203.0.113.5 dev eth1 onlink table 1\n' 'post-down /sbin/ip route del 203.0.115.1/32 ' - 'via 203.0.113.5 dev eth1 onlink table 1\n' + 'via 203.0.113.5 dev eth1 onlink table 1\n\n\n' 'post-up /sbin/ip rule add from 203.0.113.2/32 table 1 ' 'priority 100\n' 'post-down /sbin/ip rule del from 203.0.113.2/32 table 1 ' @@ -1792,21 +1792,21 @@ class TestServerTestCase(base.TestCase): handle.write.assert_any_call( '\n# Generated by Octavia agent\n' 'auto {netns_int} {netns_int}:0\n\n' - 'iface {netns_int} inet dhcp\n\n' + 'iface {netns_int} inet dhcp\n' 'iface {netns_int}:0 inet static\n' 'address 203.0.113.2\n' 'broadcast 203.0.113.255\n' - 'netmask 255.255.255.0\n' + 'netmask 255.255.255.0\n\n' '# Add a source routing table to allow members to ' - 'access the VIP\n' - 'post-up /sbin/ip route add 203.0.113.0/24 ' - 'dev eth1 src 203.0.113.2 scope link table 1\n' + 'access the VIP\n\n' 'post-up /sbin/ip route add default via 203.0.113.1 ' 'dev eth1 onlink table 1\n' 'post-down /sbin/ip route del default via 203.0.113.1 ' - 'dev eth1 onlink table 1\n' - 'post-down /sbin/ip route del 203.0.113.0/24 ' + 'dev eth1 onlink table 1\n\n\n' + 'post-up /sbin/ip route add 203.0.113.0/24 ' 'dev eth1 src 203.0.113.2 scope link table 1\n' + 'post-down /sbin/ip route del 203.0.113.0/24 ' + 'dev eth1 src 203.0.113.2 scope link table 1\n\n\n' 'post-up /sbin/ip rule add from 203.0.113.2/32 table 1 ' 'priority 100\n' 'post-down /sbin/ip rule del from 203.0.113.2/32 table 1 ' @@ -2029,20 +2029,19 @@ class TestServerTestCase(base.TestCase): 'dev {netns_int}\n' 'down route del -host 2001:db9::1/128 gw 2001:db8::5 ' 'dev {netns_int}\n' - '\n' 'iface {netns_int}:0 inet6 static\n' 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' - 'netmask 32\n' + 'netmask 32\n\n' '# Add a source routing table to allow members to access ' - 'the VIP\n' - 'post-up /sbin/ip -6 route add 2001:db8::/32 ' - 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' - 'scope link table 1\n' + 'the VIP\n\n' 'post-up /sbin/ip -6 route add default via 2001:db8::1 ' 'dev eth1 onlink table 1\n' 'post-down /sbin/ip -6 route del default via 2001:db8::1 ' - 'dev eth1 onlink table 1\n' + 'dev eth1 onlink table 1\n\n\n' + 'post-up /sbin/ip -6 route add 2001:db8::/32 ' + 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' + 'scope link table 1\n' 'post-down /sbin/ip -6 route del 2001:db8::/32 ' 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' 'scope link table 1\n' @@ -2053,12 +2052,12 @@ class TestServerTestCase(base.TestCase): 'post-up /sbin/ip -6 route add 2001:db9::1/128 via ' '2001:db8::5 dev eth1 onlink table 1\n' 'post-down /sbin/ip -6 route del 2001:db9::1/128 ' - 'via 2001:db8::5 dev eth1 onlink table 1\n' + 'via 2001:db8::5 dev eth1 onlink table 1\n\n\n' 'post-up /sbin/ip -6 rule add from ' - '2001:0db8:0000:0000:0000:0000:0000:0002/32 table 1 ' + '2001:0db8:0000:0000:0000:0000:0000:0002/128 table 1 ' 'priority 100\n' 'post-down /sbin/ip -6 rule del from ' - '2001:0db8:0000:0000:0000:0000:0000:0002/32 table 1 ' + '2001:0db8:0000:0000:0000:0000:0000:0002/128 table 1 ' 'priority 100'.format( netns_int=consts.NETNS_PRIMARY_INTERFACE)) elif distro == consts.CENTOS: @@ -2135,28 +2134,28 @@ class TestServerTestCase(base.TestCase): handle.write.assert_any_call( '\n# Generated by Octavia agent\n' 'auto {netns_int} {netns_int}:0\n\n' - 'iface {netns_int} inet6 auto\n\n' + 'iface {netns_int} inet6 auto\n' 'iface {netns_int}:0 inet6 static\n' 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' - 'netmask 32\n' + 'netmask 32\n\n' '# Add a source routing table to allow members to access ' - 'the VIP\n' - 'post-up /sbin/ip -6 route add 2001:db8::/32 ' - 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' - 'scope link table 1\n' + 'the VIP\n\n' 'post-up /sbin/ip -6 route add default via 2001:db8::1 ' 'dev eth1 onlink table 1\n' 'post-down /sbin/ip -6 route del default via 2001:db8::1 ' - 'dev eth1 onlink table 1\n' - 'post-down /sbin/ip -6 route del 2001:db8::/32 ' + 'dev eth1 onlink table 1\n\n\n' + 'post-up /sbin/ip -6 route add 2001:db8::/32 ' 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' 'scope link table 1\n' + 'post-down /sbin/ip -6 route del 2001:db8::/32 ' + 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' + 'scope link table 1\n\n\n' 'post-up /sbin/ip -6 rule add from ' - '2001:0db8:0000:0000:0000:0000:0000:0002/32 table 1 ' + '2001:0db8:0000:0000:0000:0000:0000:0002/128 table 1 ' 'priority 100\n' 'post-down /sbin/ip -6 rule del from ' - '2001:0db8:0000:0000:0000:0000:0000:0002/32 table 1 ' + '2001:0db8:0000:0000:0000:0000:0000:0002/128 table 1 ' 'priority 100'.format( netns_int=consts.NETNS_PRIMARY_INTERFACE)) elif distro == consts.CENTOS: diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py index 62c93a9870..876015b102 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py @@ -17,9 +17,12 @@ import subprocess import mock import netifaces +from oslo_config import cfg +from oslo_config import fixture as oslo_fixture from octavia.amphorae.backends.agent.api_server import osutils from octavia.amphorae.backends.agent.api_server import plug +from octavia.common import constants import octavia.tests.unit.base as base FAKE_CIDR_IPV4 = '10.0.0.0/24' @@ -104,6 +107,9 @@ class TestPlug(base.TestCase): def test_plug_vip_ipv6(self, mock_makedirs, mock_copytree, mock_check_output, mock_netns, mock_netns_create, mock_pyroute2, mock_webob, mock_nspopen): + conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + conf.config(group='controller_worker', + loadbalancer_topology=constants.TOPOLOGY_ACTIVE_STANDBY) m = mock.mock_open() with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): self.test_plug.plug_vip( diff --git a/octavia/tests/unit/amphorae/backends/agent/test_agent_jinja_cfg.py b/octavia/tests/unit/amphorae/backends/agent/test_agent_jinja_cfg.py index 3a2a2793fb..b4d5bfcd4e 100644 --- a/octavia/tests/unit/amphorae/backends/agent/test_agent_jinja_cfg.py +++ b/octavia/tests/unit/amphorae/backends/agent/test_agent_jinja_cfg.py @@ -79,7 +79,9 @@ class AgentJinjaTestCase(base.TestCase): 'agent_server_network_dir = ' '/etc/network/interfaces.d/\n' 'agent_request_read_timeout = 120\n' - 'amphora_id = ' + AMP_ID) + 'amphora_id = ' + AMP_ID + '\n\n' + '[controller_worker]\n' + 'loadbalancer_topology = SINGLE') agent_cfg = ajc.build_agent_config(AMP_ID) self.assertEqual(expected_config, agent_cfg) @@ -114,6 +116,8 @@ class AgentJinjaTestCase(base.TestCase): 'agent_server_network_file = ' '/etc/network/interfaces\n' 'agent_request_read_timeout = 120\n' - 'amphora_id = ' + AMP_ID) + 'amphora_id = ' + AMP_ID + '\n\n' + '[controller_worker]\n' + 'loadbalancer_topology = SINGLE') agent_cfg = ajc.build_agent_config(AMP_ID) self.assertEqual(expected_config, agent_cfg) diff --git a/octavia/tests/unit/amphorae/drivers/keepalived/jinja/test_jinja_cfg.py b/octavia/tests/unit/amphorae/drivers/keepalived/jinja/test_jinja_cfg.py index c895ff22d5..f3a154ad6f 100644 --- a/octavia/tests/unit/amphorae/drivers/keepalived/jinja/test_jinja_cfg.py +++ b/octavia/tests/unit/amphorae/drivers/keepalived/jinja/test_jinja_cfg.py @@ -13,6 +13,8 @@ # under the License. # +import copy + import mock from oslo_config import cfg from oslo_config import fixture as oslo_fixture @@ -73,6 +75,7 @@ class TestVRRPRestDriver(base.TestCase): " virtual_router_id 1\n" " priority 100\n" " nopreempt\n" + " accept\n" " garp_master_refresh 5\n" " garp_master_refresh_repeat 2\n" " advert_int 10\n" @@ -88,12 +91,76 @@ class TestVRRPRestDriver(base.TestCase): "\n" " virtual_ipaddress {\n" " 10.1.0.5\n" - " }\n" + " }\n\n" + " virtual_routes {\n" + " 10.1.0.0/24 dev eth1 src 10.1.0.5 scope link " + "table 1\n" + " }\n\n" + " virtual_rules {\n" + " from 10.1.0.5/32 table 1 priority 100\n" + " }\n\n" " track_script {\n" " check_script\n" " }\n" - "}\n") + "}") + + self.amphora1v6 = copy.deepcopy(self.amphora1) + self.amphora1v6.vrrp_ip = '2001:db8::10' + self.amphora2v6 = copy.deepcopy(self.amphora2) + self.amphora2v6.vrrp_ip = '2001:db8::11' + self.lbv6 = copy.deepcopy(self.lb) + self.lbv6.amphorae = [self.amphora1v6, self.amphora2v6] + self.lbv6.vip.ip_address = '2001:db8::15' + + self.ref_v6_conf = ("vrrp_script check_script {\n" + " script /tmp/test/vrrp/check_script.sh\n" + " interval 5\n" + " fall 2\n" + " rise 2\n" + "}\n" + "\n" + "vrrp_instance TESTGROUP {\n" + " state MASTER\n" + " interface eth1\n" + " virtual_router_id 1\n" + " priority 100\n" + " nopreempt\n" + " accept\n" + " garp_master_refresh 5\n" + " garp_master_refresh_repeat 2\n" + " advert_int 10\n" + " authentication {\n" + " auth_type PASS\n" + " auth_pass TESTPASSWORD\n" + " }\n" + "\n" + " unicast_src_ip 2001:db8::10\n" + " unicast_peer {\n" + " 2001:db8::11\n" + " }\n" + "\n" + " virtual_ipaddress {\n" + " 2001:db8::15\n" + " }\n\n" + " virtual_routes {\n" + " 2001:db8::/64 dev eth1 src " + "2001:db8::15 scope link table 1\n" + " }\n\n" + " virtual_rules {\n" + " from 2001:db8::15/128 table 1 " + "priority 100\n" + " }\n\n" + " track_script {\n" + " check_script\n" + " }\n" + "}") def test_build_keepalived_config(self): - config = self.templater.build_keepalived_config(self.lb, self.amphora1) + config = self.templater.build_keepalived_config( + self.lb, self.amphora1, '10.1.0.0/24') self.assertEqual(self.ref_conf, config) + + def test_build_keepalived_ipv6_config(self): + config = self.templater.build_keepalived_config( + self.lbv6, self.amphora1v6, '2001:db8::/64') + self.assertEqual(self.ref_v6_conf, config) diff --git a/octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py b/octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py index bf1d3f1bde..2512262202 100644 --- a/octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py +++ b/octavia/tests/unit/amphorae/drivers/keepalived/test_vrrp_rest_driver.py @@ -14,6 +14,7 @@ # import mock +from oslo_utils import uuidutils from octavia.amphorae.drivers.keepalived import vrrp_rest_driver from octavia.common import constants @@ -29,8 +30,14 @@ class TestVRRPRestDriver(base.TestCase): self.FAKE_CONFIG = 'FAKE CONFIG' self.lb_mock = mock.MagicMock() self.amphora_mock = mock.MagicMock() + self.amphora_mock.id = uuidutils.generate_uuid() self.amphora_mock.status = constants.AMPHORA_ALLOCATED self.lb_mock.amphorae = [self.amphora_mock] + self.amphorae_network_config = {} + vip_subnet = mock.MagicMock() + vip_subnet.cidr = '192.0.2.0/24' + self.amphorae_network_config[self.amphora_mock.id] = vip_subnet + super(TestVRRPRestDriver, self).setUp() @mock.patch('octavia.amphorae.drivers.keepalived.jinja.' @@ -39,7 +46,8 @@ class TestVRRPRestDriver(base.TestCase): mock_templater.return_value = self.FAKE_CONFIG - self.keepalived_mixin.update_vrrp_conf(self.lb_mock) + self.keepalived_mixin.update_vrrp_conf(self.lb_mock, + self.amphorae_network_config) self.client.upload_vrrp_config.assert_called_once_with( self.amphora_mock, diff --git a/octavia/tests/unit/controller/worker/flows/test_amphora_flows.py b/octavia/tests/unit/controller/worker/flows/test_amphora_flows.py index 5d5a293468..57ce767d80 100644 --- a/octavia/tests/unit/controller/worker/flows/test_amphora_flows.py +++ b/octavia/tests/unit/controller/worker/flows/test_amphora_flows.py @@ -355,7 +355,7 @@ class TestAmphoraFlows(base.TestCase): self.assertIn(constants.LOADBALANCER, vrrp_subflow.requires) self.assertEqual(1, len(vrrp_subflow.provides)) - self.assertEqual(1, len(vrrp_subflow.requires)) + self.assertEqual(2, len(vrrp_subflow.requires)) def test_get_post_map_lb_subflow(self, mock_get_net_driver): diff --git a/octavia/tests/unit/controller/worker/tasks/test_amphora_driver_tasks.py b/octavia/tests/unit/controller/worker/tasks/test_amphora_driver_tasks.py index 6e30a740b7..015a8781f0 100644 --- a/octavia/tests/unit/controller/worker/tasks/test_amphora_driver_tasks.py +++ b/octavia/tests/unit/controller/worker/tasks/test_amphora_driver_tasks.py @@ -564,10 +564,12 @@ class TestAmphoraDriverTasks(base.TestCase): mock_listener_repo_get, mock_listener_repo_update, mock_amphora_repo_update): + amphorae_network_config = mock.MagicMock() amphora_vrrp_update_obj = ( amphora_driver_tasks.AmphoraVRRPUpdate()) - amphora_vrrp_update_obj.execute(_LB_mock) - mock_driver.update_vrrp_conf.assert_called_once_with(_LB_mock) + amphora_vrrp_update_obj.execute(_LB_mock, amphorae_network_config) + mock_driver.update_vrrp_conf.assert_called_once_with( + _LB_mock, amphorae_network_config) def test_amphora_vrrp_stop(self, mock_driver, diff --git a/releasenotes/notes/fix_active_standby_ipv6-0317d5cd9e5d50e5.yaml b/releasenotes/notes/fix_active_standby_ipv6-0317d5cd9e5d50e5.yaml new file mode 100644 index 0000000000..3bd08e2271 --- /dev/null +++ b/releasenotes/notes/fix_active_standby_ipv6-0317d5cd9e5d50e5.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + To resolve the IPv6 VIP issues on active/standby load balancers you + need to build a new amphora image. +fixes: + - | + Fixes issues using IPv6 VIP addresses with load balancers configured for + active/standby topology. This fix requires a new amphora image to be + built.