From a890f2ba35fed4424cf9dcff8224d699431beb45 Mon Sep 17 00:00:00 2001 From: ZhaoBo Date: Thu, 21 Dec 2017 10:52:46 +0800 Subject: [PATCH] UDP for [2] These files will split with the current Octavia repo, before other parts are ok. Patch List: [1] Finish keepalived LVS jinja template for UDP support [2] Extend the ability of amp agent for upload/refresh the keepalived process [3] Extend the db model and db table with necessary fields for met the new udp backend [4] Add logic/workflow elements process in UDP cases [5] Extend the existing API to access udp parameters in Listener API [6] Extend the existing pool API to access the new option in session_persistence fields Change-Id: Ib4924e602d450b1feadb29e830d715ae77f5bbfe --- etc/octavia.conf | 4 + .../backends/agent/agent_jinja_cfg.py | 3 +- .../backends/agent/api_server/amphora_info.py | 112 +++-- .../agent/api_server/keepalivedlvs.py | 352 +++++++++++++++ .../backends/agent/api_server/listener.py | 4 +- .../backends/agent/api_server/osutils.py | 44 ++ .../backends/agent/api_server/plug.py | 16 + .../backends/agent/api_server/server.py | 52 ++- .../templates/amphora-netns.systemd.j2 | 6 + .../templates/keepalived.systemd.j2 | 4 + .../templates/keepalived.sysvinit.j2 | 4 + .../templates/keepalived.upstart.j2 | 4 + .../keepalived_lvs_check_script.sh.j2 | 21 + .../templates/plug_port_ethX.conf.j2 | 2 + .../templates/plug_vip_ethX.conf.j2 | 2 + .../rh_plug_port_eth_ifdown_local.conf.j2 | 19 + .../rh_plug_port_eth_ifup_local.conf.j2 | 19 + .../agent/api_server/udp_listener_base.py | 123 +++++ .../backends/agent/api_server/util.py | 75 ++++ .../templates/amphora_agent_conf.template | 1 + .../backends/health_daemon/health_daemon.py | 26 ++ .../backends/utils/keepalivedlvs_query.py | 421 +++++++++++++++++ .../drivers/haproxy/rest_api_driver.py | 128 ++++-- octavia/common/config.py | 3 + octavia/common/constants.py | 5 + .../healthmanager/health_drivers/update_db.py | 25 ++ .../controller/worker/flows/amphora_flows.py | 17 +- .../drivers/neutron/allowed_address_pairs.py | 29 +- .../backend/agent/api_server/test_server.py | 129 +++++- .../agent/api_server/test_amphora_info.py | 218 +++++++++ .../agent/api_server/test_keepalivedlvs.py | 422 ++++++++++++++++++ .../backends/agent/api_server/test_plug.py | 35 +- .../backends/agent/test_agent_jinja_cfg.py | 43 +- .../health_daemon/test_health_daemon.py | 60 +++ .../utils/test_keepalivedlvs_query.py | 397 ++++++++++++++++ .../drivers/haproxy/test_rest_api_driver.py | 176 +++++++- .../health_drivers/test_update_db.py | 51 +++ .../neutron/test_allowed_address_pairs.py | 53 ++- setup.cfg | 2 + 39 files changed, 2959 insertions(+), 148 deletions(-) create mode 100644 octavia/amphorae/backends/agent/api_server/keepalivedlvs.py create mode 100644 octavia/amphorae/backends/agent/api_server/templates/keepalived_lvs_check_script.sh.j2 create mode 100644 octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifdown_local.conf.j2 create mode 100644 octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifup_local.conf.j2 create mode 100644 octavia/amphorae/backends/agent/api_server/udp_listener_base.py create mode 100644 octavia/amphorae/backends/utils/keepalivedlvs_query.py create mode 100644 octavia/tests/unit/amphorae/backends/agent/api_server/test_keepalivedlvs.py create mode 100644 octavia/tests/unit/amphorae/backends/utils/test_keepalivedlvs_query.py diff --git a/etc/octavia.conf b/etc/octavia.conf index 03feff76e8..493b027bfe 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -310,6 +310,10 @@ # agent_server_network_file = # agent_request_read_timeout = 120 +# Amphora default UDP driver is keepalived_lvs +# +# amphora_udp_driver = keepalived_lvs + [keepalived_vrrp] # Amphora Role/Priority advertisement interval in seconds # vrrp_advert_int = 1 diff --git a/octavia/amphorae/backends/agent/agent_jinja_cfg.py b/octavia/amphorae/backends/agent/agent_jinja_cfg.py index 5a151bee93..72fbe92bf9 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, + 'amphora_udp_driver': CONF.amphora_agent.amphora_udp_driver}) diff --git a/octavia/amphorae/backends/agent/api_server/amphora_info.py b/octavia/amphorae/backends/agent/api_server/amphora_info.py index 91b449d9fd..a20fa07218 100644 --- a/octavia/amphorae/backends/agent/api_server/amphora_info.py +++ b/octavia/amphorae/backends/agent/api_server/amphora_info.py @@ -32,48 +32,68 @@ class AmphoraInfo(object): def __init__(self, osutils): self._osutils = osutils - def compile_amphora_info(self): - return webob.Response( - json={'hostname': socket.gethostname(), - 'haproxy_version': - self._get_version_of_installed_package('haproxy'), - 'api_version': api_server.VERSION}) + def compile_amphora_info(self, extend_udp_driver=None): + extend_body = {} + if extend_udp_driver: + extend_body = self._get_extend_body_from_udp_driver( + extend_udp_driver) + body = {'hostname': socket.gethostname(), + 'haproxy_version': + self._get_version_of_installed_package('haproxy'), + 'api_version': api_server.VERSION} + if extend_body: + body.update(extend_body) + return webob.Response(json=body) - def compile_amphora_details(self): - listener_list = util.get_listeners() + def compile_amphora_details(self, extend_udp_driver=None): + haproxy_listener_list = util.get_listeners() + extend_body = {} + udp_listener_list = [] + if extend_udp_driver: + udp_listener_list = util.get_udp_listeners() + extend_data = self._get_extend_body_from_udp_driver( + extend_udp_driver) + udp_count = self._count_udp_listener_processes(extend_udp_driver, + udp_listener_list) + extend_body['udp_listener_process_count'] = udp_count + extend_body.update(extend_data) meminfo = self._get_meminfo() cpu = self._cpu() st = os.statvfs('/') - return webob.Response( - json={'hostname': socket.gethostname(), - 'haproxy_version': - self._get_version_of_installed_package('haproxy'), - 'api_version': api_server.VERSION, - 'networks': self._get_networks(), - 'active': True, - 'haproxy_count': - self._count_haproxy_processes(listener_list), - 'cpu': { - 'total': cpu['total'], - 'user': cpu['user'], - 'system': cpu['system'], - 'soft_irq': cpu['softirq'], }, - 'memory': { - 'total': meminfo['MemTotal'], - 'free': meminfo['MemFree'], - 'buffers': meminfo['Buffers'], - 'cached': meminfo['Cached'], - 'swap_used': meminfo['SwapCached'], - 'shared': meminfo['Shmem'], - 'slab': meminfo['Slab'], }, - 'disk': { - 'used': (st.f_blocks - st.f_bfree) * st.f_frsize, - 'available': st.f_bavail * st.f_frsize}, - 'load': self._load(), - 'topology': consts.TOPOLOGY_SINGLE, - 'topology_status': consts.TOPOLOGY_STATUS_OK, - 'listeners': listener_list, - 'packages': {}}) + body = {'hostname': socket.gethostname(), + 'haproxy_version': + self._get_version_of_installed_package('haproxy'), + 'api_version': api_server.VERSION, + 'networks': self._get_networks(), + 'active': True, + 'haproxy_count': + self._count_haproxy_processes(haproxy_listener_list), + 'cpu': { + 'total': cpu['total'], + 'user': cpu['user'], + 'system': cpu['system'], + 'soft_irq': cpu['softirq'], }, + 'memory': { + 'total': meminfo['MemTotal'], + 'free': meminfo['MemFree'], + 'buffers': meminfo['Buffers'], + 'cached': meminfo['Cached'], + 'swap_used': meminfo['SwapCached'], + 'shared': meminfo['Shmem'], + 'slab': meminfo['Slab'], }, + 'disk': { + 'used': (st.f_blocks - st.f_bfree) * st.f_frsize, + 'available': st.f_bavail * st.f_frsize}, + 'load': self._load(), + 'topology': consts.TOPOLOGY_SINGLE, + 'topology_status': consts.TOPOLOGY_STATUS_OK, + 'listeners': list( + set(haproxy_listener_list + udp_listener_list)) + if udp_listener_list else haproxy_listener_list, + 'packages': {}} + if extend_body: + body.update(extend_body) + return webob.Response(json=body) def _get_version_of_installed_package(self, name): @@ -89,6 +109,22 @@ class AmphoraInfo(object): num += 1 return num + def _count_udp_listener_processes(self, udp_driver, listener_list): + num = 0 + for listener_id in listener_list: + if udp_driver.is_listener_running(listener_id): + # optional check if it's still running + num += 1 + return num + + def _get_extend_body_from_udp_driver(self, extend_udp_driver): + extend_info = extend_udp_driver.get_subscribed_amp_compile_info() + extend_data = {} + for extend in extend_info: + package_version = self._get_version_of_installed_package(extend) + extend_data['%s_version' % extend] = package_version + return extend_data + def _get_meminfo(self): re_parser = re.compile(r'^(?P\S*):\s*(?P\d*)\s*kB') result = dict() diff --git a/octavia/amphorae/backends/agent/api_server/keepalivedlvs.py b/octavia/amphorae/backends/agent/api_server/keepalivedlvs.py new file mode 100644 index 0000000000..e396015bd8 --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/keepalivedlvs.py @@ -0,0 +1,352 @@ +# Copyright 2011-2014 OpenStack Foundation +# +# 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 os +import re +import shutil +import stat +import subprocess + +import flask +import jinja2 +from oslo_log import log as logging +import webob +from werkzeug import exceptions + +from octavia.amphorae.backends.agent.api_server import listener +from octavia.amphorae.backends.agent.api_server import udp_listener_base +from octavia.amphorae.backends.agent.api_server import util +from octavia.amphorae.backends.utils import keepalivedlvs_query +from octavia.common import constants as consts + +BUFFER = 100 +CHECK_SCRIPT_NAME = 'udp_check.sh' +KEEPALIVED_CHECK_SCRIPT_NAME = 'lvs_udp_check.sh' +LOG = logging.getLogger(__name__) + +j2_env = jinja2.Environment(autoescape=True, loader=jinja2.FileSystemLoader( + os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES)) +UPSTART_TEMPLATE = j2_env.get_template(consts.KEEPALIVED_JINJA2_UPSTART) +SYSVINIT_TEMPLATE = j2_env.get_template(consts.KEEPALIVED_JINJA2_SYSVINIT) +SYSTEMD_TEMPLATE = j2_env.get_template(consts.KEEPALIVED_JINJA2_SYSTEMD) +check_script_file_template = j2_env.get_template( + consts.KEEPALIVED_CHECK_SCRIPT) + + +class KeepalivedLvs(udp_listener_base.UdpListenerApiServerBase): + + _SUBSCRIBED_AMP_COMPILE = ['keepalived', 'ipvsadm'] + + def upload_udp_listener_config(self, listener_id): + stream = listener.Wrapped(flask.request.stream) + NEED_CHECK = True + + if not os.path.exists(util.keepalived_lvs_dir()): + os.makedirs(util.keepalived_lvs_dir()) + if not os.path.exists(util.keepalived_backend_check_script_dir()): + current_file_dir, _ = os.path.split(os.path.abspath(__file__)) + + try: + script_dir = os.path.join(os.path.abspath( + os.path.join(current_file_dir, '../..')), 'utils') + assert True is os.path.exists(script_dir) + assert True is os.path.exists(os.path.join( + script_dir, CHECK_SCRIPT_NAME)) + except Exception: + raise exceptions.Conflict( + description='%(file_name)s not Found for ' + 'UDP Listener %(listener_id)s' % + {'file_name': CHECK_SCRIPT_NAME, + 'listener_id': listener_id}) + os.makedirs(util.keepalived_backend_check_script_dir()) + shutil.copy2(os.path.join(script_dir, CHECK_SCRIPT_NAME), + util.keepalived_backend_check_script_path()) + os.chmod(util.keepalived_backend_check_script_path(), stat.S_IEXEC) + # Based on current topology setting, only the amphora instances in + # Active-Standby topology will create the directory below. So for + # Single topology, it should not create the directory and the check + # scripts for status change. + if not os.path.exists(util.keepalived_check_scripts_dir()): + NEED_CHECK = False + + conf_file = util.keepalived_lvs_cfg_path(listener_id) + 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 + with os.fdopen(os.open(conf_file, flags, mode), 'wb') as f: + b = stream.read(BUFFER) + while b: + f.write(b) + b = stream.read(BUFFER) + + init_system = util.get_os_init_system() + + file_path = util.keepalived_lvs_init_path(init_system, listener_id) + + if init_system == consts.INIT_SYSTEMD: + template = SYSTEMD_TEMPLATE + init_enable_cmd = ("systemctl enable " + "octavia-keepalivedlvs-%s" + % str(listener_id)) + elif init_system == consts.INIT_UPSTART: + template = UPSTART_TEMPLATE + elif init_system == consts.INIT_SYSVINIT: + template = SYSVINIT_TEMPLATE + init_enable_cmd = "insserv {file}".format(file=file_path) + else: + raise util.UnknownInitError() + + if init_system == consts.INIT_SYSTEMD: + # mode 00644 + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + else: + # mode 00755 + mode = (stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | + stat.S_IROTH | stat.S_IXOTH) + keepalived_pid, vrrp_pid, check_pid = util.keepalived_lvs_pids_path( + listener_id) + if not os.path.exists(file_path): + with os.fdopen(os.open(file_path, flags, mode), 'w') as text_file: + text = template.render( + keepalived_pid=keepalived_pid, + vrrp_pid=vrrp_pid, + check_pid=check_pid, + keepalived_cmd=consts.KEEPALIVED_CMD, + keepalived_cfg=util.keepalived_lvs_cfg_path(listener_id), + amphora_nsname=consts.AMPHORA_NAMESPACE + ) + text_file.write(text) + + # Make sure the new service is enabled on boot + if init_system != consts.INIT_UPSTART: + try: + subprocess.check_output(init_enable_cmd.split(), + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + LOG.debug('Failed to enable ' + 'octavia-keepalivedlvs service: ' + '%(err)s', {'err': e}) + return webob.Response(json=dict( + message="Error enabling " + "octavia-keepalivedlvs service", + details=e.output), status=500) + + if NEED_CHECK: + # inject the check script for keepalived process + script_path = os.path.join(util.keepalived_check_scripts_dir(), + KEEPALIVED_CHECK_SCRIPT_NAME) + if not os.path.exists(script_path): + with os.fdopen(os.open(script_path, flags, stat.S_IEXEC), + 'w') as script_file: + text = check_script_file_template.render( + consts=consts, + init_system=init_system, + keepalived_lvs_pid_dir=util.keepalived_lvs_dir() + ) + script_file.write(text) + + res = webob.Response(json={'message': 'OK'}, status=200) + res.headers['ETag'] = stream.get_md5() + return res + + def _check_udp_listener_exists(self, listener_id): + if not os.path.exists(util.keepalived_lvs_cfg_path(listener_id)): + raise exceptions.HTTPException( + response=webob.Response(json=dict( + message='UDP Listener Not Found', + details="No UDP listener with UUID: {0}".format( + listener_id)), status=404)) + + def get_udp_listener_config(self, listener_id): + """Gets the keepalivedlvs config + + :param listener_id: the id of the listener + """ + self._check_udp_listener_exists(listener_id) + with open(util.keepalived_lvs_cfg_path(listener_id), 'r') as file: + cfg = file.read() + resp = webob.Response(cfg, content_type='text/plain') + return resp + + def manage_udp_listener(self, listener_id, action): + action = action.lower() + if action not in [consts.AMP_ACTION_START, + consts.AMP_ACTION_STOP, + consts.AMP_ACTION_RELOAD]: + return webob.Response(json=dict( + message='Invalid Request', + details="Unknown action: {0}".format(action)), status=400) + + self._check_udp_listener_exists(listener_id) + if action == consts.AMP_ACTION_RELOAD: + if consts.OFFLINE == self._check_udp_listener_status(listener_id): + action = consts.AMP_ACTION_START + + cmd = ("/usr/sbin/service " + "octavia-keepalivedlvs-{listener_id} " + "{action}".format(listener_id=listener_id, action=action)) + + try: + subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + LOG.debug('Failed to %s keepalivedlvs listener %s', + listener_id + ' : ' + action, e) + return webob.Response(json=dict( + message=("Failed to {0} keepalivedlvs listener {1}" + .format(action, listener_id)), + details=e.output), status=500) + + return webob.Response( + json=dict(message='OK', + details='keepalivedlvs listener {listener_id}' + '{action}ed'.format(listener_id=listener_id, + action=action)), + status=202) + + def _check_udp_listener_status(self, listener_id): + if os.path.exists(util.keepalived_lvs_pids_path(listener_id)[0]): + if os.path.exists(os.path.join( + '/proc', util.get_keepalivedlvs_pid(listener_id))): + # Check if the listener is disabled + with open(util.keepalived_lvs_cfg_path(listener_id), + 'r') as file: + cfg = file.read() + m = re.search('virtual_server', cfg) + if m: + return consts.ACTIVE + return consts.OFFLINE + return consts.ERROR + return consts.OFFLINE + + def get_all_udp_listeners_status(self): + """Gets the status of all UDP listeners + + This method will not consult the stats socket + so a listener might show as ACTIVE but still be + in ERROR + """ + listeners = list() + + for udp_listener in util.get_udp_listeners(): + status = self._check_udp_listener_status(udp_listener) + listeners.append({ + 'status': status, + 'uuid': udp_listener, + 'type': 'lvs', + }) + return listeners + + def get_udp_listener_status(self, listener_id): + """Gets the status of a UDP listener + + This method will consult the stats socket + so calling this method will interfere with + the health daemon with the risk of the amphora + shut down + + :param listener_id: The id of the listener + """ + self._check_udp_listener_exists(listener_id) + + status = self._check_udp_listener_status(listener_id) + + if status != consts.ACTIVE: + stats = dict( + status=status, + uuid=listener_id, + type='' + ) + return webob.Response(json=stats) + + stats = dict( + status=status, + uuid=listener_id, + type='lvs' + ) + + try: + pool = keepalivedlvs_query.get_udp_listener_pool_status( + listener_id) + except subprocess.CalledProcessError as e: + return webob.Response(json=dict( + message="Error get kernel lvs status for udp listener", + details=e.output), status=500) + stats['pools'] = [pool] + return webob.Response(json=stats) + + def delete_udp_listener(self, listener_id): + try: + self._check_udp_listener_exists(listener_id) + except exceptions.HTTPException: + return webob.Response(json={'message': 'OK'}) + + # check if that keepalived is still running and if stop it + keepalived_pid, vrrp_pid, check_pid = util.keepalived_lvs_pids_path( + listener_id) + if os.path.exists(keepalived_pid) and os.path.exists( + os.path.join('/proc', + util.get_keepalivedlvs_pid(listener_id))): + cmd = ("/usr/sbin/service " + "octavia-keepalivedlvs-{0} stop".format(listener_id)) + try: + subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + LOG.error("Failed to stop keepalivedlvs service: %s", e) + return webob.Response(json=dict( + message="Error stopping keepalivedlvs", + details=e.output), status=500) + + # Since the lvs check script based on the keepalived pid file for + # checking whether it is alived. So here, we had stop the keepalived + # process by the previous step, must make sure the pid files are not + # exist. + if (os.path.exists(keepalived_pid) or + os.path.exists(vrrp_pid) or os.path.exists(check_pid)): + for pid in [keepalived_pid, vrrp_pid, check_pid]: + os.remove(pid) + + # disable the service + init_system = util.get_os_init_system() + init_path = util.keepalived_lvs_init_path(init_system, listener_id) + + if init_system == consts.INIT_SYSTEMD: + init_disable_cmd = ( + "systemctl disable octavia-keepalivedlvs-" + "{list}".format(list=listener_id)) + elif init_system == consts.INIT_SYSVINIT: + init_disable_cmd = "insserv -r {file}".format(file=init_path) + elif init_system != consts.INIT_UPSTART: + raise util.UnknownInitError() + + if init_system != consts.INIT_UPSTART: + try: + subprocess.check_output(init_disable_cmd.split(), + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + LOG.error("Failed to disable " + "octavia-keepalivedlvs-%(list)s service: " + "%(err)s", {'list': listener_id, 'err': e}) + return webob.Response(json=dict( + message=( + "Error disabling octavia-keepalivedlvs-" + "{0} service".format(listener_id)), + details=e.output), status=500) + + # delete init script ,config file and log file for that listener + if os.path.exists(init_path): + os.remove(init_path) + if os.path.exists(util.keepalived_lvs_cfg_path(listener_id)): + os.remove(util.keepalived_lvs_cfg_path(listener_id)) + + return webob.Response(json={'message': 'OK'}) diff --git a/octavia/amphorae/backends/agent/api_server/listener.py b/octavia/amphorae/backends/agent/api_server/listener.py index 720a7a36cd..c2790db59d 100644 --- a/octavia/amphorae/backends/agent/api_server/listener.py +++ b/octavia/amphorae/backends/agent/api_server/listener.py @@ -362,7 +362,7 @@ class Listener(object): return webob.Response(json={'message': 'OK'}) - def get_all_listeners_status(self): + def get_all_listeners_status(self, other_listeners=None): """Gets the status of all listeners This method will not consult the stats socket @@ -386,6 +386,8 @@ class Listener(object): 'type': listener_type, }) + if other_listeners: + listeners = listeners + other_listeners return webob.Response(json=listeners, content_type='application/json') def get_listener_status(self, listener_id): diff --git a/octavia/amphorae/backends/agent/api_server/osutils.py b/octavia/amphorae/backends/agent/api_server/osutils.py index 739861582e..6b6d7d8ffd 100644 --- a/octavia/amphorae/backends/agent/api_server/osutils.py +++ b/octavia/amphorae/backends/agent/api_server/osutils.py @@ -315,6 +315,10 @@ class RH(BaseOS): ETH_X_ALIAS_VIP_CONF = 'rh_plug_vip_ethX_alias.conf.j2' ROUTE_ETH_X_CONF = 'rh_route_ethX.conf.j2' RULE_ETH_X_CONF = 'rh_rule_ethX.conf.j2' + # The reason of make them as jinja templates is the current scripts force + # to add the iptables, so leave it now for future extending if possible. + ETH_IFUP_LOCAL_SCRIPT = 'rh_plug_port_eth_ifup_local.conf.j2' + ETH_IFDOWN_LOCAL_SCRIPT = 'rh_plug_port_eth_ifdown_local.conf.j2' @classmethod def is_os_name(cls, os_name): @@ -411,6 +415,8 @@ class RH(BaseOS): route_rules_interface_file_path, primary_interface, render_host_routes, template_rules, gateway, vip, netmask) + self._write_ifup_ifdown_local_scripts_if_possible() + def write_static_routes_interface_file(self, interface_file_path, interface, host_routes, template_routes, gateway, @@ -460,6 +466,7 @@ class RH(BaseOS): self.write_static_routes_interface_file( routes_interface_file_path, netns_interface, host_routes, template_routes, None, None, None) + self._write_ifup_ifdown_local_scripts_if_possible() def bring_interfaces_up(self, ip, primary_interface, secondary_interface): if ip.version == 4: @@ -473,6 +480,43 @@ class RH(BaseOS): def has_ifup_all(self): return False + def _write_ifup_ifdown_local_scripts_if_possible(self): + if self._check_ifup_ifdown_local_scripts_exists(): + template_ifup_local = j2_env.get_template( + self.ETH_IFUP_LOCAL_SCRIPT) + self.write_port_interface_if_local_scripts(template_ifup_local) + template_ifdown_local = j2_env.get_template( + self.ETH_IFDOWN_LOCAL_SCRIPT) + self.write_port_interface_if_local_scripts(template_ifdown_local, + ifup=False) + + def _check_ifup_ifdown_local_scripts_exists(self): + file_names = ['ifup-local', 'ifdown-local'] + target_dir = '/sbin/' + res = [] + for file_name in file_names: + if os.path.exists(os.path.join(target_dir, file_name)): + res.append(True) + else: + res.append(False) + + # This means we only add the scripts when both of them are non-exists + return not any(res) + + def write_port_interface_if_local_scripts( + self, template_script, ifup=True): + file_name = 'ifup' + '-local' + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + if not ifup: + file_name = 'ifdown' + '-local' + with os.fdopen( + os.open(os.path.join( + '/sbin/', file_name), flags, mode), 'w') as text_file: + text = template_script.render() + text_file.write(text) + os.chmod(os.path.join('/sbin/', file_name), stat.S_IEXEC) + class CentOS(RH): diff --git a/octavia/amphorae/backends/agent/api_server/plug.py b/octavia/amphorae/backends/agent/api_server/plug.py index 56fc9c44b2..1c2ed11906 100644 --- a/octavia/amphorae/backends/agent/api_server/plug.py +++ b/octavia/amphorae/backends/agent/api_server/plug.py @@ -127,10 +127,26 @@ class Plug(object): sysctl = pyroute2.NSPopen(consts.AMPHORA_NAMESPACE, [consts.SYSCTL_CMD, '--system'], stdout=subprocess.PIPE) + sysctl.communicate() sysctl.wait() sysctl.release() + cmd_list = [['modprobe', 'ip_vs'], + [consts.SYSCTL_CMD, '-w', 'net.ipv4.vs.conntrack=1']] + if ip.version == 4: + # For lvs function, enable ip_vs kernel module, enable ip_forward + # conntrack in amphora network namespace. + cmd_list.append([consts.SYSCTL_CMD, '-w', 'net.ipv4.ip_forward=1']) + elif ip.version == 6: + cmd_list.append([consts.SYSCTL_CMD, '-w', + 'net.ipv6.conf.all.forwarding=1']) + for cmd in cmd_list: + ns_exec = pyroute2.NSPopen(consts.AMPHORA_NAMESPACE, cmd, + stdout=subprocess.PIPE) + ns_exec.wait() + ns_exec.release() + with pyroute2.IPRoute() as ipr: # Move the interfaces into the namespace idx = ipr.link_lookup(ifname=default_netns_interface)[0] diff --git a/octavia/amphorae/backends/agent/api_server/server.py b/octavia/amphorae/backends/agent/api_server/server.py index 3964e2547b..b26a4d8ec3 100644 --- a/octavia/amphorae/backends/agent/api_server/server.py +++ b/octavia/amphorae/backends/agent/api_server/server.py @@ -25,6 +25,7 @@ from octavia.amphorae.backends.agent.api_server import keepalived from octavia.amphorae.backends.agent.api_server import listener from octavia.amphorae.backends.agent.api_server import osutils from octavia.amphorae.backends.agent.api_server import plug +from octavia.amphorae.backends.agent.api_server import udp_listener_base PATH_PREFIX = '/' + api_server.VERSION @@ -42,12 +43,25 @@ def register_app_error_handler(app): app.register_error_handler(code, make_json_error) +def check_and_return_request_listener_protocol(request): + try: + protocol_dict = request.get_json() + assert type(protocol_dict) is dict + assert 'protocol' in protocol_dict + except Exception: + raise exceptions.BadRequest( + description='Invalid protocol information for Listener') + return protocol_dict['protocol'] + + class Server(object): def __init__(self): self.app = flask.Flask(__name__) self._osutils = osutils.BaseOS.get_os_util() self._keepalived = keepalived.Keepalived() self._listener = listener.Listener() + self._udp_listener = (udp_listener_base.UdpListenerApiServerBase. + get_server_driver()) self._plug = plug.Plug(self._osutils) self._amphora_info = amphora_info.AmphoraInfo(self._osutils) @@ -57,10 +71,19 @@ class Server(object): '/listeners///haproxy', view_func=self.upload_haproxy_config, methods=['PUT']) + self.app.add_url_rule(rule=PATH_PREFIX + + '/listeners//' + '/udp_listener', + view_func=self.upload_udp_listener_config, + methods=['PUT']) self.app.add_url_rule(rule=PATH_PREFIX + '/listeners//haproxy', view_func=self.get_haproxy_config, methods=['GET']) + self.app.add_url_rule(rule=PATH_PREFIX + + '/listeners//udp_listener', + view_func=self.get_udp_listener_config, + methods=['GET']) self.app.add_url_rule(rule=PATH_PREFIX + '/listeners//', view_func=self.start_stop_listener, @@ -113,25 +136,48 @@ class Server(object): def upload_haproxy_config(self, amphora_id, listener_id): return self._listener.upload_haproxy_config(amphora_id, listener_id) + def upload_udp_listener_config(self, amphora_id, listener_id): + return self._udp_listener.upload_udp_listener_config(listener_id) + def get_haproxy_config(self, listener_id): return self._listener.get_haproxy_config(listener_id) + def get_udp_listener_config(self, listener_id): + return self._udp_listener.get_udp_listener_config(listener_id) + def start_stop_listener(self, listener_id, action): + protocol = check_and_return_request_listener_protocol( + flask.request) + if protocol == 'UDP': + return self._udp_listener.manage_udp_listener( + listener_id, action) return self._listener.start_stop_listener(listener_id, action) def delete_listener(self, listener_id): + protocol = check_and_return_request_listener_protocol( + flask.request) + if protocol == 'UDP': + return self._udp_listener.delete_udp_listener(listener_id) return self._listener.delete_listener(listener_id) def get_details(self): - return self._amphora_info.compile_amphora_details() + return self._amphora_info.compile_amphora_details( + extend_udp_driver=self._udp_listener) def get_info(self): - return self._amphora_info.compile_amphora_info() + return self._amphora_info.compile_amphora_info( + extend_udp_driver=self._udp_listener) def get_all_listeners_status(self): - return self._listener.get_all_listeners_status() + udp_listeners = self._udp_listener.get_all_udp_listeners_status() + return self._listener.get_all_listeners_status( + other_listeners=udp_listeners) def get_listener_status(self, listener_id): + protocol = check_and_return_request_listener_protocol( + flask.request) + if protocol == 'UDP': + return self._udp_listener.get_udp_listener_status(listener_id) return self._listener.get_listener_status(listener_id) def upload_certificate(self, listener_id, filename): diff --git a/octavia/amphorae/backends/agent/api_server/templates/amphora-netns.systemd.j2 b/octavia/amphorae/backends/agent/api_server/templates/amphora-netns.systemd.j2 index 94688915c0..c31296ff02 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/amphora-netns.systemd.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/amphora-netns.systemd.j2 @@ -10,6 +10,12 @@ RemainAfterExit=yes ExecStart=-/sbin/ip netns add {{ amphora_nsname }} # Load the system sysctl into the new namespace ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} sysctl --system +# Enable kernel module ip_vs for lvs function in amphora network namespace +ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} modprobe ip_vs +# Enable ip_forward and conntrack kernel configuration +ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} sysctl -w net.ipv4.ip_forward=1 +ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} sysctl -w net.ipv4.vs.conntrack=1 +ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} sysctl -w net.ipv6.conf.all.forwarding=1 # We need the plugged_interfaces file sorted to join the host interfaces ExecStart=-/bin/sh -c '/usr/bin/sort -k 1 /var/lib/octavia/plugged_interfaces > /var/lib/octavia/plugged_interfaces.sorted' # Assign the interfaces into the namespace with the appropriate name diff --git a/octavia/amphorae/backends/agent/api_server/templates/keepalived.systemd.j2 b/octavia/amphorae/backends/agent/api_server/templates/keepalived.systemd.j2 index 79919ec3c7..d743f7371b 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/keepalived.systemd.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/keepalived.systemd.j2 @@ -8,7 +8,11 @@ Wants=network-online.target SELinuxContext=system_u:system_r:keepalived_t:s0 Type=forking KillMode=process +{% if vrrp_pid and check_pid %} +ExecStart=/sbin/ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }} -D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }} -r {{ vrrp_pid }} -c {{ check_pid }} +{% else %} ExecStart=/sbin/ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }} -D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }} +{% endif %} ExecReload=/bin/kill -HUP $MAINPID PIDFile={{ keepalived_pid }} diff --git a/octavia/amphorae/backends/agent/api_server/templates/keepalived.sysvinit.j2 b/octavia/amphorae/backends/agent/api_server/templates/keepalived.sysvinit.j2 index 297d6cde0a..9689a5ca62 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/keepalived.sysvinit.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/keepalived.sysvinit.j2 @@ -18,7 +18,11 @@ DAEMON="ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }}" NAME=octavia-keepalived DESC=octavia-keepalived TMPFILES="/tmp/.vrrp /tmp/.healthcheckers" +{% if vrrp_pid and check_pid %} +DAEMON_ARGS="-D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }} -r {{ vrrp_pid }} -c {{ check_pid }}" +{% else %} DAEMON_ARGS="-D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }}" +{% endif %} #includes lsb functions . /lib/lsb/init-functions diff --git a/octavia/amphorae/backends/agent/api_server/templates/keepalived.upstart.j2 b/octavia/amphorae/backends/agent/api_server/templates/keepalived.upstart.j2 index fd72b62611..554005169a 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/keepalived.upstart.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/keepalived.upstart.j2 @@ -22,4 +22,8 @@ stop on runlevel [!2345] respawn +{% if vrrp_pid and check_pid %} +exec /sbin/ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }} -n -D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }} -r {{ vrrp_pid }} -c {{ check_pid }} +{% else %} exec /sbin/ip netns exec {{ amphora_nsname }} {{ keepalived_cmd }} -n -D -d -f {{ keepalived_cfg }} -p {{ keepalived_pid }} +{% endif %} diff --git a/octavia/amphorae/backends/agent/api_server/templates/keepalived_lvs_check_script.sh.j2 b/octavia/amphorae/backends/agent/api_server/templates/keepalived_lvs_check_script.sh.j2 new file mode 100644 index 0000000000..46d8ef33cc --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/templates/keepalived_lvs_check_script.sh.j2 @@ -0,0 +1,21 @@ +#!/bin/bash + +# Don't try to run the directory when it is empty +shopt -s nullglob + +status=0 +for file in {{ keepalived_lvs_pid_dir }}/* +do + file_ext=${file#*.} + case $file_ext in + pid) echo "Check keepalived pid file: " $file;; + *) continue;; + esac + {% if init_system == consts.INIT_SYSTEMD %} + systemctl status $(basename $file .pid) > /dev/null + {% elif init_system in (consts.INIT_UPSTART, consts.INIT_SYSVINIT) %} + kill -0 `cat $file` + {% endif %} + status=$(( $status + $? )) +done +exit $status diff --git a/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 index d10f965e16..633e30cceb 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 @@ -27,6 +27,8 @@ mtu {{ mtu }} up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} {%- endfor %} +post-up /sbin/ip{{ '6' if ipv6 }}tables -t nat -A POSTROUTING -p udp -o {{ interface }} -j MASQUERADE +post-down /sbin/ip{{ '6' if ipv6 }}tables -t nat -D POSTROUTING -p udp -o {{ interface }} -j MASQUERADE {%- else %} iface {{ interface }} inet dhcp auto {{ interface }}:0 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 6aa78f426c..0fccdd82a9 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 @@ -53,3 +53,5 @@ post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del {{ hr.network }} via {{ hr.g {%- 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 +post-up /sbin/ip{{ '6' if vip_ipv6 }}tables -t nat -A POSTROUTING -p udp -o {{ interface }} -j MASQUERADE +post-down /sbin/ip{{ '6' if vip_ipv6 }}tables -t nat -D POSTROUTING -p udp -o {{ interface }} -j MASQUERADE diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifdown_local.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifdown_local.conf.j2 new file mode 100644 index 0000000000..f1bea6067a --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifdown_local.conf.j2 @@ -0,0 +1,19 @@ +{# 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. +#} +# Generated by Octavia agent +#!/bin/bash +if [[ "$1" != "lo" ]] + then + /sbin/iptables -t nat -D POSTROUTING -o $1 -p udp -j MASQUERADE + /sbin/ip6tables -t nat -D POSTROUTING -o $1 -p udp -j MASQUERADE +fi diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifup_local.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifup_local.conf.j2 new file mode 100644 index 0000000000..cb364f8209 --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifup_local.conf.j2 @@ -0,0 +1,19 @@ +{# 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. +#} +# Generated by Octavia agent +#!/bin/bash +if [[ "$1" != "lo" ]] + then + /sbin/iptables -t nat -A POSTROUTING -o $1 -p udp -j MASQUERADE + /sbin/ip6tables -t nat -A POSTROUTING -o $1 -p udp -j MASQUERADE +fi diff --git a/octavia/amphorae/backends/agent/api_server/udp_listener_base.py b/octavia/amphorae/backends/agent/api_server/udp_listener_base.py new file mode 100644 index 0000000000..a5ce9ef896 --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/udp_listener_base.py @@ -0,0 +1,123 @@ +# Copyright 2018 OpenStack Foundation +# +# 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 abc + +import six + +from oslo_config import cfg +from stevedore import driver as stevedore_driver + +CONF = cfg.CONF +UDP_SERVER_NAMESPACE = 'octavia.amphora.udp_api_server' + + +@six.add_metaclass(abc.ABCMeta) +class UdpListenerApiServerBase(object): + """Base UDP Listener Server API + + """ + + _SUBSCRIBED_AMP_COMPILE = [] + SERVER_INSTANCE = None + + @classmethod + def get_server_driver(cls): + if not cls.SERVER_INSTANCE: + cls.SERVER_INSTANCE = stevedore_driver.DriverManager( + namespace=UDP_SERVER_NAMESPACE, + name=CONF.amphora_agent.amphora_udp_driver, + invoke_on_load=True, + ).driver + return cls.SERVER_INSTANCE + + def get_subscribed_amp_compile_info(self): + return self._SUBSCRIBED_AMP_COMPILE + + @abc.abstractmethod + def upload_udp_listener_config(self, listener_id): + """Upload the configuration for UDP. + + :param listener_id: The id of a UDP Listener + + :returns: HTTP response with status code. + :raises Exception: If any file / directory is not found or + fail to create. + + """ + pass + + @abc.abstractmethod + def get_udp_listener_config(self, listener_id): + """Gets the UDP Listener configuration details + + :param listener_id: the id of the UDP Listener + + :returns: HTTP response with status code. + :raises Exception: If the listener is failed to find. + + """ + pass + + @abc.abstractmethod + def manage_udp_listener(self, listener_id, action): + """Gets the UDP Listener configuration details + + :param listener_id: the id of the UDP Listener + :param action: the operation type. + + :returns: HTTP response with status code. + :raises Exception: If the listener is failed to find. + + """ + pass + + @abc.abstractmethod + def get_all_udp_listeners_status(self): + """Gets the status of all UDP Listeners + + This method will not consult the stats socket + so a listener might show as ACTIVE but still be + in ERROR + + :returns: a list of UDP Listener status + :raises Exception: If the listener pid located directory is not exist + + """ + pass + + @abc.abstractmethod + def get_udp_listener_status(self, listener_id): + """Gets the status of a UDP listener + + :param listener_id: The id of the listener + + :returns: HTTP response with status code. + :raises Exception: If the listener is failed to find. + + """ + pass + + @abc.abstractmethod + def delete_udp_listener(self, listener_id): + """Delete a UDP Listener from a amphora + + :param listener_id: The id of the listener + + :returns: HTTP response with status code. + :raises Exception: If unsupport initial system of amphora. + + """ + pass diff --git a/octavia/amphorae/backends/agent/api_server/util.py b/octavia/amphorae/backends/agent/api_server/util.py index aaf1a8f2bb..4049dad53a 100644 --- a/octavia/amphorae/backends/agent/api_server/util.py +++ b/octavia/amphorae/backends/agent/api_server/util.py @@ -14,6 +14,7 @@ import os +import re import subprocess from oslo_config import cfg @@ -41,6 +42,56 @@ def init_path(listener_id, init_system): raise UnknownInitError() +def keepalived_lvs_dir(): + return os.path.join(CONF.haproxy_amphora.base_path, 'lvs') + + +def keepalived_lvs_init_path(init_system, listener_id): + if init_system == consts.INIT_SYSTEMD: + return os.path.join(consts.SYSTEMD_DIR, + consts.KEEPALIVED_SYSTEMD_PREFIX % + str(listener_id)) + elif init_system == consts.INIT_UPSTART: + return os.path.join(consts.UPSTART_DIR, + consts.KEEPALIVED_UPSTART_PREFIX % + str(listener_id)) + elif init_system == consts.INIT_SYSVINIT: + return os.path.join(consts.SYSVINIT_DIR, + consts.KEEPALIVED_SYSVINIT_PREFIX % + str(listener_id)) + else: + raise UnknownInitError() + + +def keepalived_backend_check_script_dir(): + return os.path.join(CONF.haproxy_amphora.base_path, 'lvs/check/') + + +def keepalived_backend_check_script_path(): + return os.path.join(keepalived_backend_check_script_dir(), + 'udp_check.sh') + + +def keepalived_lvs_pids_path(listener_id): + pids_path = {} + for file_ext in ['pid', 'vrrp.pid', 'check.pid']: + pids_path[file_ext] = ( + os.path.join(CONF.haproxy_amphora.base_path, + ('lvs/octavia-keepalivedlvs-%s.%s') % + (str(listener_id), file_ext))) + return pids_path['pid'], pids_path['vrrp.pid'], pids_path['check.pid'] + + +def keepalived_lvs_cfg_path(listener_id): + return os.path.join(CONF.haproxy_amphora.base_path, + ('lvs/octavia-keepalivedlvs-%s.conf') % + str(listener_id)) + + +def keepalived_lvs_iptables_dir(): + return os.path.join(CONF.haproxy_amphora.base_path, 'lvs/iptables/') + + def haproxy_dir(listener_id): return os.path.join(CONF.haproxy_amphora.base_path, listener_id) @@ -58,6 +109,12 @@ def get_haproxy_pid(listener_id): return f.readline().rstrip() +def get_keepalivedlvs_pid(listener_id): + pid_file, _, _ = keepalived_lvs_pids_path(listener_id) + with open(pid_file, 'r') as f: + return f.readline().rstrip() + + def haproxy_sock_path(listener_id): return os.path.join(CONF.haproxy_amphora.base_path, listener_id + '.sock') @@ -125,6 +182,24 @@ def is_listener_running(listener_id): os.path.join('/proc', get_haproxy_pid(listener_id))) +def get_udp_listeners(): + result = [] + if os.path.exists(keepalived_lvs_dir()): + for f in os.listdir(keepalived_lvs_dir()): + if f.endswith('.conf'): + prefix = f.split('.')[0] + if re.search("octavia-keepalivedlvs-", prefix): + result.append(f.split( + 'octavia-keepalivedlvs-')[1].split('.')[0]) + return result + + +def is_udp_listener_running(listener_id): + pid_file, _, _ = keepalived_lvs_pids_path(listener_id) + return os.path.exists(pid_file) and os.path.exists( + os.path.join('/proc', get_keepalivedlvs_pid(listener_id))) + + def get_os_init_system(): if os.path.exists(consts.INIT_PROC_COMM_PATH): with open(consts.INIT_PROC_COMM_PATH, 'r') as init_comm: diff --git a/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template b/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template index cca80bd28a..749fb5a5f4 100644 --- a/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template +++ b/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template @@ -42,3 +42,4 @@ agent_server_network_file = {{ agent_server_network_file }} {% endif -%} agent_request_read_timeout = {{ agent_request_read_timeout }} amphora_id = {{ amphora_id }} +amphora_udp_driver = {{ amphora_udp_driver }} diff --git a/octavia/amphorae/backends/health_daemon/health_daemon.py b/octavia/amphorae/backends/health_daemon/health_daemon.py index d3753199f6..3d687384ea 100644 --- a/octavia/amphorae/backends/health_daemon/health_daemon.py +++ b/octavia/amphorae/backends/health_daemon/health_daemon.py @@ -25,6 +25,8 @@ import six from octavia.amphorae.backends.agent.api_server import util from octavia.amphorae.backends.health_daemon import health_sender from octavia.amphorae.backends.utils import haproxy_query +from octavia.amphorae.backends.utils import keepalivedlvs_query + if six.PY2: import Queue as queue # pylint: disable=wrong-import-order @@ -143,4 +145,28 @@ def build_stats_message(): pools = listener_dict['pools'] pools[pool_id] = {"status": pool['status'], "members": pool['members']} + + # UDP listener part + udp_listener_ids = util.get_udp_listeners() + if udp_listener_ids: + listeners_stats = keepalivedlvs_query.get_udp_listeners_stats() + if listeners_stats: + for listener_id, listener_stats in listeners_stats.items(): + pool_status = keepalivedlvs_query.get_udp_listener_pool_status( + listener_id) + udp_listener_dict = dict() + udp_listener_dict['status'] = listener_stats['status'] + udp_listener_dict['stats'] = { + 'tx': listener_stats['stats']['bout'], + 'rx': listener_stats['stats']['bin'], + 'conns': listener_stats['stats']['scur'], + 'totconns': listener_stats['stats']['stot'], + 'ereq': listener_stats['stats']['ereq'] + } + if pool_status: + udp_listener_dict['pools'] = { + pool_status['lvs']['uuid']: { + "status": pool_status['lvs']['status'], + "members": pool_status['lvs']['members']}} + msg['listeners'][listener_id] = udp_listener_dict return msg diff --git a/octavia/amphorae/backends/utils/keepalivedlvs_query.py b/octavia/amphorae/backends/utils/keepalivedlvs_query.py new file mode 100644 index 0000000000..b3a21be7d3 --- /dev/null +++ b/octavia/amphorae/backends/utils/keepalivedlvs_query.py @@ -0,0 +1,421 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re +import subprocess + +import netaddr +from oslo_log import log as logging + +from octavia.amphorae.backends.agent.api_server import util +from octavia.common import constants + +LOG = logging.getLogger(__name__) +KERNEL_LVS_PATH = '/proc/net/ip_vs' +KERNEL_LVS_STATS_PATH = '/proc/net/ip_vs_stats' +LVS_KEY_REGEX = re.compile(r"RemoteAddress:Port\s+(.*$)") +V4_RS_VALUE_REGEX = re.compile(r"(\w{8}:\w{4})\s+(.*$)") +V4_HEX_IP_REGEX = re.compile(r"(\w{2})(\w{2})(\w{2})(\w{2})") +V6_RS_VALUE_REGEX = re.compile(r"(\[[[\w{4}:]+\b\]:\w{4})\s+(.*$)") + +NS_REGEX = re.compile(r"net_namespace\s(\w+-\w+)") +V4_VS_REGEX = re.compile(r"virtual_server\s([\d+\.]+\b)\s(\d{1,5})") +V4_RS_REGEX = re.compile(r"real_server\s([\d+\.]+\b)\s(\d{1,5})") +V6_VS_REGEX = re.compile(r"virtual_server\s([\w*:]+\b)\s(\d{1,5})") +V6_RS_REGEX = re.compile(r"real_server\s([\w*:]+\b)\s(\d{1,5})") +CONFIG_COMMENT_REGEX = re.compile( + r"#\sConfiguration\sfor\s(\w+)\s(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})") + + +def read_kernel_file(ns_name, file_path): + cmd = ("ip netns exec {ns} cat {lvs_stat_path}".format( + ns=ns_name, lvs_stat_path=file_path)) + try: + output = subprocess.check_output(cmd.split(), + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + LOG.error("Failed to get kernel lvs status in ns %(ns_name)s " + "%(kernel_lvs_path)s: %(err)s %(out)s", + {'ns_name': ns_name, 'kernel_lvs_path': file_path, + 'err': e, 'out': e.output}) + raise e + # py3 treat the output as bytes type. + if isinstance(output, bytes): + output = output.decode('utf-8') + return output + + +def get_listener_realserver_mapping(ns_name, listener_ip_port): + # returned result: + # actual_member_result = {'rs_ip:listened_port': { + # 'status': 'UP', + # 'Forward': forward_type, + # 'Weight': 5, + # 'ActiveConn': 0, + # 'InActConn': 0 + # }} + try: + listener_ip, listener_port = listener_ip_port.split(':') + except ValueError: + start = listener_ip_port.index('[') + 1 + end = listener_ip_port.index(']') + listener_ip = listener_ip_port[start:end] + listener_port = listener_ip_port[end + 2:] + ip_obj = netaddr.IPAddress(listener_ip) + output = read_kernel_file(ns_name, KERNEL_LVS_PATH).split('\n') + ip_to_hex_format = '' + if ip_obj.version == 4: + for int_str in listener_ip.split('.'): + if int(int_str) <= 15: + str_piece = '0' + hex(int(int_str))[2:].upper() + else: + str_piece = hex(int(int_str))[2:].upper() + ip_to_hex_format += str_piece + elif ip_obj.version == 6: + piece_list = [] + for word in ip_obj.words: + str_len = len(hex(word)[2:]) + if str_len < 4: + str_piece = '0' * (4 - str_len) + hex(word)[2:].lower() + else: + str_piece = hex(word)[2:].lower() + piece_list.append(str_piece) + ip_to_hex_format = ":".join(piece_list) + ip_to_hex_format = '\[' + ip_to_hex_format + '\]' + port_hex_format = hex(int(listener_port))[2:].upper() + if len(port_hex_format) < 4: + port_hex_format = ('0' * (4 - len(port_hex_format)) + + port_hex_format) + idex = ip_to_hex_format + ':' + port_hex_format + + def _hit_identify(line): + m = re.match(r'^UDP\s+%s\s+\w+' % idex, line) + if m: + return True + return False + actual_member_result = {} + find_target_block = False + result_keys = [] + for line in output: + if 'RemoteAddress:Port' in line: + result_keys = re.split(r'\s+', + LVS_KEY_REGEX.findall(line)[0].strip()) + elif line.startswith('UDP') and find_target_block: + break + elif line.startswith('UDP') and _hit_identify(line): + find_target_block = True + elif find_target_block and line: + rs_is_ipv4 = True + all_values = V4_RS_VALUE_REGEX.findall(line) + # If can not get all_values with ipv4 regex, then this line must be + # a ipv6 real server record. + if not all_values: + all_values = V6_RS_VALUE_REGEX.findall(line) + rs_is_ipv4 = False + + all_values = all_values[0] + ip_port = all_values[0] + result_values = re.split(r"\s+", all_values[1].strip()) + if rs_is_ipv4: + actual_member_ip_port = ip_port.split(':') + hex_ip_list = V4_HEX_IP_REGEX.findall( + actual_member_ip_port[0])[0] + ip_string = '' + for hex_ip in hex_ip_list: + ip_string = ip_string + str(int(hex_ip, 16)) + '.' + ip_string = ip_string[:-1] + port_string = str(int(actual_member_ip_port[1], 16)) + member_ip_port_string = ip_string + ':' + port_string + else: + start = ip_port.index('[') + 1 + end = ip_port.index(']') + ip_string = ip_port[start:end] + port_string = ip_port[end + 2:] + member_ip_port_string = '[' + str( + netaddr.IPAddress(ip_string)) + ']:' + str( + int(port_string, 16)) + result_key_count = len(result_keys) + for index in range(result_key_count): + if member_ip_port_string not in actual_member_result: + actual_member_result[ + member_ip_port_string] = {'status': constants.UP, + result_keys[index]: + result_values[index]} + else: + # The other values include the weight + actual_member_result[ + member_ip_port_string][ + result_keys[index]] = result_values[index] + continue + + return find_target_block, actual_member_result + + +def get_udp_listener_resource_ipports_nsname(listener_id): + # resource_ipport_mapping = {'Listener': {'id': listener-id, + # 'ipport': ipport}, + # 'Pool': {'id': pool-id}, + # 'Members': [{'id': member-id-1, + # 'ipport': ipport}, + # {'id': member-id-2, + # 'ipport': ipport}], + # 'HealthMonitor': {'id': healthmonitor-id}} + resource_ipport_mapping = {} + with open(util.keepalived_lvs_cfg_path(listener_id), 'r') as f: + cfg = f.read() + ns_name = NS_REGEX.findall(cfg)[0] + listener_ip_port = V4_VS_REGEX.findall(cfg) + if not listener_ip_port: + listener_ip_port = V6_VS_REGEX.findall(cfg) + listener_ip_port = listener_ip_port[0] if listener_ip_port else [] + + if not listener_ip_port: + # If not get listener_ip_port from the lvs config file, + # that means the udp listener's default pool have no enabled member + # yet. But at this moment, we can get listener_id and ns_name, so + # for this function, we will just return ns_name + return resource_ipport_mapping, ns_name + + cfg_line = cfg.split('\n') + rs_ip_port_list = [] + for line in cfg_line: + if 'real_server' in line: + res = V4_RS_REGEX.findall(line) + if not res: + res = V6_RS_REGEX.findall(line) + rs_ip_port_list.append(res[0]) + + resource_type_ids = CONFIG_COMMENT_REGEX.findall(cfg) + + for resource_type, resource_id in resource_type_ids: + value = {'id': resource_id} + if resource_type == 'Member': + resource_type = '%ss' % resource_type + if resource_type not in resource_ipport_mapping: + value = [value] + if resource_type not in resource_ipport_mapping: + resource_ipport_mapping[resource_type] = value + elif resource_type == 'Members': + resource_ipport_mapping[resource_type].append(value) + + if rs_ip_port_list: + rs_ip_port_count = len(rs_ip_port_list) + for index in range(rs_ip_port_count): + if netaddr.IPAddress( + rs_ip_port_list[index][0]).version == 6: + rs_ip_port_list[index] = ( + '[' + rs_ip_port_list[index][0] + ']', + rs_ip_port_list[index][1]) + resource_ipport_mapping['Members'][index]['ipport'] = ( + rs_ip_port_list[index][0] + ':' + + rs_ip_port_list[index][1]) + + if netaddr.IPAddress(listener_ip_port[0]).version == 6: + listener_ip_port = ( + '[' + listener_ip_port[0] + ']', listener_ip_port[1]) + resource_ipport_mapping['Listener']['ipport'] = ( + listener_ip_port[0] + ':' + listener_ip_port[1]) + + return resource_ipport_mapping, ns_name + + +def get_udp_listener_pool_status(listener_id): + (resource_ipport_mapping, + ns_name) = get_udp_listener_resource_ipports_nsname(listener_id) + if 'Pool' not in resource_ipport_mapping: + return {} + elif 'Members' not in resource_ipport_mapping: + return {'lvs': { + 'uuid': resource_ipport_mapping['Pool']['id'], + 'status': constants.DOWN, + 'members': {} + }} + + _, realserver_result = get_listener_realserver_mapping( + ns_name, resource_ipport_mapping['Listener']['ipport']) + pool_status = constants.UP + member_results = {} + if realserver_result: + member_ip_port_list = [ + member['ipport'] for member in resource_ipport_mapping['Members']] + down_member_ip_port_set = set( + member_ip_port_list) - set(list(realserver_result.keys())) + + for member_ip_port in member_ip_port_list: + member_id = None + for member in resource_ipport_mapping['Members']: + if member['ipport'] == member_ip_port: + member_id = member['id'] + if member_ip_port in down_member_ip_port_set: + status = constants.DOWN + elif int(realserver_result[member_ip_port]['Weight']) == 0: + status = constants.DRAIN + else: + status = realserver_result[member_ip_port]['status'] + + if member_id: + member_results[member_id] = status + else: + pool_status = constants.DOWN + for member in resource_ipport_mapping['Members']: + member_results[member['id']] = constants.DOWN + + return { + 'lvs': + { + 'uuid': resource_ipport_mapping['Pool']['id'], + 'status': pool_status, + 'members': member_results + } + } + + +def get_ipvsadm_info(ns_name, is_stats_cmd=False): + cmd_list = ['ip', 'netns', 'exec', ns_name, 'ipvsadm', '-Ln'] + if is_stats_cmd: + cmd_list.append('--stats') + output = subprocess.check_output(cmd_list, stderr=subprocess.STDOUT) + if isinstance(output, bytes): + output = output.decode('utf-8') + output = output.split('\n') + fields = [] + # mapping = {'listeneripport': {'Linstener': vs_values, + # 'members': [rs_values1, rs_values2]}} + last_key = None + value_mapping = dict() + output_line_num = len(output) + + def split_line(line): + return re.sub(r'\s+', ' ', line.strip()).split(' ') + for line_num in range(output_line_num): + # ipvsadm -Ln + if 'Flags' in output[line_num]: + fields = split_line(output[line_num]) + elif fields and 'Flags' in fields and fields.index('Flags') == len( + fields) - 1: + fields.extend(split_line(output[line_num])) + # ipvsadm -Ln --stats + elif 'Prot' in output[line_num]: + fields = split_line(output[line_num]) + elif 'RemoteAddress' in output[line_num]: + start = fields.index('LocalAddress:Port') + 1 + temp_fields = fields[start:] + fields.extend(split_line(output[line_num])) + fields.extend(temp_fields) + # here we get the all fields + elif constants.PROTOCOL_UDP in output[line_num]: + # if UDP/TCP in this line, we can know this line is + # VS configuration. + vs_values = split_line(output[line_num]) + for value in vs_values: + if ':' in value: + value_mapping[value] = {'Listener': vs_values, + 'Members': []} + last_key = value + break + # here the line must be a RS which belongs to a VS + elif '->' in output[line_num] and last_key: + rs_values = split_line(output[line_num]) + rs_values.remove('->') + value_mapping[last_key]['Members'].append(rs_values) + + index = fields.index('->') + vs_fields = fields[:index] + if 'Flags' in vs_fields: + vs_fields.remove('Flags') + rs_fields = fields[index + 1:] + for key in list(value_mapping.keys()): + value_mapping[key]['Listener'] = [ + i for i in zip(vs_fields, value_mapping[key]['Listener'])] + member_res = [] + for member_value in value_mapping[key]['Members']: + member_res.append([i for i in zip(rs_fields, member_value)]) + value_mapping[key]['Members'] = member_res + + return value_mapping + + +def get_udp_listeners_stats(): + udp_listener_ids = util.get_udp_listeners() + need_check_listener_ids = [ + listener_id for listener_id in udp_listener_ids + if util.is_udp_listener_running(listener_id)] + ipport_mapping = dict() + for check_listener_id in need_check_listener_ids: + # resource_ipport_mapping = {'Listener': {'id': listener-id, + # 'ipport': ipport}, + # 'Pool': {'id': pool-id}, + # 'Members': [{'id': member-id-1, + # 'ipport': ipport}, + # {'id': member-id-2, + # 'ipport': ipport}], + # 'HealthMonitor': {'id': healthmonitor-id}} + (resource_ipport_mapping, + ns_name) = get_udp_listener_resource_ipports_nsname(check_listener_id) + # If we can not read the lvs configuration from file, that means + # the pool of this listener may own zero enabled member, but the + # keepalived process is running. So we need to skip it. + if not resource_ipport_mapping: + continue + ipport_mapping.update({check_listener_id: resource_ipport_mapping}) + + # So here, if we can not get any ipport_mapping, + # we do nothing, just return + if not ipport_mapping: + return None + + # contains bout, bin, scur, stot, ereq, status + # bout(OutBytes), bin(InBytes), stot(Conns) from cmd ipvsadm -Ln --stats + # scur(ActiveConn) from cmd ipvsadm -Ln + # status, can see configuration in any cmd, treat it as OPEN + # ereq is still 0, as UDP case does not support it. + scur_res = get_ipvsadm_info(constants.AMPHORA_NAMESPACE) + stats_res = get_ipvsadm_info(constants.AMPHORA_NAMESPACE, + is_stats_cmd=True) + listener_stats_res = dict() + for listener_id, ipport in ipport_mapping.items(): + listener_ipport = ipport['Listener']['ipport'] + # This would be in Error, wait for the next loop to sync for the + # listener at this moment. Also this is for skip the case no enabled + # member in UDP listener, so we don't check it for failover. + if listener_ipport not in scur_res or listener_ipport not in stats_res: + continue + + scur, bout, bin, stot, ereq = 0, 0, 0, 0, 0 + # As all results contain this listener, so its status should be OPEN + status = constants.OPEN + # Get scur + for m in scur_res[listener_ipport]['Members']: + for item in m: + if item[0] == 'ActiveConn': + scur += int(item[1]) + + # Get bout, bin, stot + for item in stats_res[listener_ipport]['Listener']: + if item[0] == 'Conns': + stot = int(item[1]) + elif item[0] == 'OutBytes': + bout = int(item[1]) + elif item[0] == 'InBytes': + bin = int(item[1]) + + listener_stats_res.update({ + listener_id: { + 'stats': { + 'bout': bout, + 'bin': bin, + 'scur': scur, + 'stot': stot, + 'ereq': ereq}, + 'status': status}}) + + return listener_stats_res diff --git a/octavia/amphorae/drivers/haproxy/rest_api_driver.py b/octavia/amphorae/drivers/haproxy/rest_api_driver.py index 328aaaeca7..740fa3f78e 100644 --- a/octavia/amphorae/drivers/haproxy/rest_api_driver.py +++ b/octavia/amphorae/drivers/haproxy/rest_api_driver.py @@ -30,6 +30,7 @@ from octavia.amphorae.drivers.keepalived import vrrp_rest_driver from octavia.common.config import cfg from octavia.common import constants as consts from octavia.common.jinja.haproxy import jinja_cfg +from octavia.common.jinja.lvs import jinja_cfg as jinja_udp_cfg from octavia.common.tls_utils import cert_parser from octavia.common import utils @@ -59,6 +60,7 @@ class HaproxyAmphoraLoadBalancerDriver( base_crt_dir=CONF.haproxy_amphora.base_cert_dir, haproxy_template=CONF.haproxy_amphora.haproxy_template, connection_logging=CONF.haproxy_amphora.connection_logging) + self.udp_jinja = jinja_udp_cfg.LvsJinjaTemplater() def update_amphora_listeners(self, listeners, amphora_index, amphorae, timeout_dict=None): @@ -87,36 +89,64 @@ class HaproxyAmphoraLoadBalancerDriver( for listener in listeners: LOG.debug("%s updating listener %s on amphora %s", self.__class__.__name__, listener.id, amp.id) - certs = self._process_tls_certificates(listener) - # Generate HaProxy configuration from listener object - config = self.jinja.build_config( - host_amphora=amp, - listener=listener, - tls_cert=certs['tls_cert'], - user_group=CONF.haproxy_amphora.user_group) - self.client.upload_config(amp, listener.id, config, - timeout_dict=timeout_dict) - self.client.reload_listener(amp, listener.id, - timeout_dict=timeout_dict) - - def update(self, listener, vip): - LOG.debug("Amphora %s haproxy, updating listener %s, vip %s", - self.__class__.__name__, listener.protocol_port, - vip.ip_address) - - # Process listener certificate info - certs = self._process_tls_certificates(listener) - - for amp in listener.load_balancer.amphorae: - if amp.status != consts.DELETED: + if listener.protocol == 'UDP': + # Generate Keepalived LVS configuration from listener object + config = self.udp_jinja.build_config(listener=listener) + self.client.upload_udp_config(amp, listener.id, config, + timeout_dict=timeout_dict) + self.client.reload_listener(amp, listener.id, + listener.protocol, + timeout_dict=timeout_dict) + else: + certs = self._process_tls_certificates(listener) # Generate HaProxy configuration from listener object config = self.jinja.build_config( host_amphora=amp, listener=listener, tls_cert=certs['tls_cert'], user_group=CONF.haproxy_amphora.user_group) - self.client.upload_config(amp, listener.id, config) - self.client.reload_listener(amp, listener.id) + self.client.upload_config(amp, listener.id, config, + timeout_dict=timeout_dict) + self.client.reload_listener(amp, listener.id, + timeout_dict=timeout_dict) + + def _udp_update(self, listener, vip): + LOG.debug("Amphora %s keepalivedlvs, updating " + "listener %s, vip %s", + self.__class__.__name__, listener.protocol_port, + vip.ip_address) + + for amp in listener.load_balancer.amphorae: + if amp.status != consts.DELETED: + # Generate Keepalived LVS configuration from listener object + config = self.udp_jinja.build_config(listener=listener) + self.client.upload_udp_config(amp, listener.id, config) + self.client.reload_listener(amp, listener.id, + listener.protocol) + + def update(self, listener, vip): + if listener.protocol == 'UDP': + self._udp_update(listener, vip) + else: + LOG.debug("Amphora %s haproxy, updating listener %s, " + "vip %s", self.__class__.__name__, + listener.protocol_port, + vip.ip_address) + + # Process listener certificate info + certs = self._process_tls_certificates(listener) + + for amp in listener.load_balancer.amphorae: + if amp.status != consts.DELETED: + # Generate HaProxy configuration from listener object + config = self.jinja.build_config( + host_amphora=amp, + listener=listener, + tls_cert=certs['tls_cert'], + user_group=CONF.haproxy_amphora.user_group) + self.client.upload_config(amp, listener.id, config) + self.client.reload_listener(amp, listener.id, + listener.protocol) def upload_cert_amp(self, amp, pem): LOG.debug("Amphora %s updating cert in REST driver " @@ -124,13 +154,37 @@ class HaproxyAmphoraLoadBalancerDriver( self.__class__.__name__, amp.id) self.client.update_cert_for_rotation(amp, pem) + def _check_if_need_add_listener_protocol(self, func): + # as _apply func will be called by create/update/delete and some cert + # related function. But there is only a port of them can accept + # listener protocol parameter, including: + # start/stop functions call _apply by functools. + # delete function call _apply directly. + # escape cert operation based on function name. + # So add this check for verify if the target function need a protocol + # parameter. + called_by_functools = (not hasattr(func, '__name__') and + hasattr(func, 'func') and + func.func.__name__.find('cert') < 0) + called_directly = (hasattr(func, '__name__') and + func.__name__.find('cert') < 0) + return called_directly or called_by_functools + def _apply(self, func, listener=None, amphora=None, *args): if amphora is None: for amp in listener.load_balancer.amphorae: if amp.status != consts.DELETED: + if self._check_if_need_add_listener_protocol(func): + _list = list(args) + _list.append(listener.protocol) + args = _list func(amp, listener.id, *args) else: if amphora.status != consts.DELETED: + if self._check_if_need_add_listener_protocol(func): + _list = list(args) + _list.append(listener.protocol) + args = _list func(amphora, listener.id, *args) def stop(self, listener, vip): @@ -374,17 +428,21 @@ class AmphoraAPIClient(object): data=config) return exc.check_exception(r) - def get_listener_status(self, amp, listener_id): + def get_listener_status(self, amp, listener_id, protocol=None): + protocol_dict = {'protocol': protocol} r = self.get( amp, - 'listeners/{listener_id}'.format(listener_id=listener_id)) + 'listeners/{listener_id}'.format(listener_id=listener_id), + json=protocol_dict) if exc.check_exception(r): return r.json() return None - def _action(self, action, amp, listener_id, timeout_dict=None): + def _action(self, action, amp, listener_id, protocol, timeout_dict=None): + protocol_dict = {'protocol': protocol} r = self.put(amp, 'listeners/{listener_id}/{action}'.format( - listener_id=listener_id, action=action), timeout_dict=timeout_dict) + listener_id=listener_id, action=action), timeout_dict=timeout_dict, + json=protocol_dict) return exc.check_exception(r) def upload_cert_pem(self, amp, listener_id, pem_filename, pem_file): @@ -407,9 +465,11 @@ class AmphoraAPIClient(object): return r.json().get("md5sum") return None - def delete_listener(self, amp, listener_id): + def delete_listener(self, amp, listener_id, protocol): + protocol_dict = {'protocol': protocol} r = self.delete( - amp, 'listeners/{listener_id}'.format(listener_id=listener_id)) + amp, 'listeners/{listener_id}'.format(listener_id=listener_id), + json=protocol_dict) return exc.check_exception(r, (404,)) @@ -463,3 +523,11 @@ class AmphoraAPIClient(object): if exc.check_exception(r): return r.json() return None + + def upload_udp_config(self, amp, listener_id, config, timeout_dict=None): + r = self.put( + amp, + 'listeners/{amphora_id}/{listener_id}/udp_listener'.format( + amphora_id=amp.id, listener_id=listener_id), timeout_dict, + data=config) + return exc.check_exception(r) diff --git a/octavia/common/config.py b/octavia/common/config.py index 6a03c14790..925d93ad5f 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -135,6 +135,9 @@ amphora_agent_opts = [ "controller to run before terminating the socket.")), # Do not specify in octavia.conf, loaded at runtime cfg.StrOpt('amphora_id', help=_("The amphora ID.")), + cfg.StrOpt('amphora_udp_driver', + default='keepalived_lvs', + help='The UDP API backend for amphora agent.'), ] networking_opts = [ diff --git a/octavia/common/constants.py b/octavia/common/constants.py index c1bb05b2f9..dd49d8c8d1 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -438,6 +438,7 @@ KEEPALIVED_JINJA2_UPSTART = 'keepalived.upstart.j2' KEEPALIVED_JINJA2_SYSTEMD = 'keepalived.systemd.j2' KEEPALIVED_JINJA2_SYSVINIT = 'keepalived.sysvinit.j2' CHECK_SCRIPT_CONF = 'keepalived_check_script.conf.j2' +KEEPALIVED_CHECK_SCRIPT = 'keepalived_lvs_check_script.sh.j2' PLUGGED_INTERFACES = '/var/lib/octavia/plugged_interfaces' HAPROXY_USER_GROUP_CFG = '/var/lib/octavia/haproxy-default-user-group.conf' @@ -482,6 +483,10 @@ KEEPALIVED_SYSTEMD = 'octavia-keepalived.service' KEEPALIVED_SYSVINIT = 'octavia-keepalived' KEEPALIVED_UPSTART = 'octavia-keepalived.conf' +KEEPALIVED_SYSTEMD_PREFIX = 'octavia-keepalivedlvs-%s.service' +KEEPALIVED_SYSVINIT_PREFIX = 'octavia-keepalivedlvs-%s' +KEEPALIVED_UPSTART_PREFIX = 'octavia-keepalivedlvs-%s.conf' + # Authentication KEYSTONE = 'keystone' NOAUTH = 'noauth' diff --git a/octavia/controller/healthmanager/health_drivers/update_db.py b/octavia/controller/healthmanager/health_drivers/update_db.py index b7c637300f..d4178b2dc5 100644 --- a/octavia/controller/healthmanager/health_drivers/update_db.py +++ b/octavia/controller/healthmanager/health_drivers/update_db.py @@ -118,6 +118,31 @@ class UpdateHealthDb(update_base.HealthUpdateBase): if db_lb: expected_listener_count = len(db_lb.listeners) + + # For udp listener, the udp health won't send out by amp agent. + # Once the default_pool of udp listener have the first enabled + # member, then the health will be sent out. So during this period, + # need to figure out the udp listener and ignore them by changing + # expected_listener_count. + for listener in db_lb.listeners: + need_remove = False + if listener.protocol == constants.PROTOCOL_UDP: + enabled_members = ([member + for member in + listener.default_pool.members + if member.enabled] + if listener.default_pool else []) + if listener.default_pool: + if not listener.default_pool.members: + need_remove = True + elif not enabled_members: + need_remove = True + else: + need_remove = True + + if need_remove: + expected_listener_count = expected_listener_count - 1 + if 'PENDING' in db_lb.provisioning_status: ignore_listener_count = True else: diff --git a/octavia/controller/worker/flows/amphora_flows.py b/octavia/controller/worker/flows/amphora_flows.py index 6a3d9056ed..308f85eb82 100644 --- a/octavia/controller/worker/flows/amphora_flows.py +++ b/octavia/controller/worker/flows/amphora_flows.py @@ -384,6 +384,16 @@ class AmphoraFlows(object): failover_amphora_flow.add(database_tasks.GetAmphoraeFromLoadbalancer( requires=constants.LOADBALANCER, provides=constants.AMPHORAE)) + # Plug the VIP ports into the new amphora + # The reason for moving these steps here is the udp listeners want to + # do some kernel configuration before Listener update for forbidding + # failure during rebuild amphora. + failover_amphora_flow.add(network_tasks.PlugVIPPort( + requires=(constants.AMPHORA, constants.AMPHORAE_NETWORK_CONFIG))) + failover_amphora_flow.add(amphora_driver_tasks.AmphoraPostVIPPlug( + requires=(constants.AMPHORA, constants.LOADBALANCER, + constants.AMPHORAE_NETWORK_CONFIG))) + # Listeners update needs to be run on all amphora to update # their peer configurations. So parallelize this with an # unordered subflow. @@ -414,13 +424,6 @@ class AmphoraFlows(object): failover_amphora_flow.add(update_amps_subflow) - # Plug the VIP ports into the new amphora - failover_amphora_flow.add(network_tasks.PlugVIPPort( - requires=(constants.AMPHORA, constants.AMPHORAE_NETWORK_CONFIG))) - failover_amphora_flow.add(amphora_driver_tasks.AmphoraPostVIPPlug( - requires=(constants.AMPHORA, constants.LOADBALANCER, - constants.AMPHORAE_NETWORK_CONFIG))) - # Plug the member networks into the new amphora failover_amphora_flow.add(network_tasks.CalculateAmphoraDelta( requires=(constants.LOADBALANCER, constants.AMPHORA), diff --git a/octavia/network/drivers/neutron/allowed_address_pairs.py b/octavia/network/drivers/neutron/allowed_address_pairs.py index f81f6528c3..f125feeba5 100644 --- a/octavia/network/drivers/neutron/allowed_address_pairs.py +++ b/octavia/network/drivers/neutron/allowed_address_pairs.py @@ -136,29 +136,41 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): rules = self.neutron_client.list_security_group_rules( security_group_id=sec_grp_id) updated_ports = [ - listener.protocol_port for listener in load_balancer.listeners + (listener.protocol_port, + constants.PROTOCOL_TCP.lower() + if listener.protocol != constants.PROTOCOL_UDP else + constants.PROTOCOL_UDP.lower()) + for listener in load_balancer.listeners if listener.provisioning_status != constants.PENDING_DELETE and listener.provisioning_status != constants.DELETED] + # As the peer port will hold the tcp connection for keepalived and + # haproxy session synchronization, so here the security group rule + # should be just related with tcp protocol only. peer_ports = [ - listener.peer_port for listener in load_balancer.listeners + (listener.peer_port, + constants.PROTOCOL_TCP.lower()) + for listener in load_balancer.listeners if listener.provisioning_status != constants.PENDING_DELETE and listener.provisioning_status != constants.DELETED] updated_ports.extend(peer_ports) # Just going to use port_range_max for now because we can assume that # port_range_max and min will be the same since this driver is # responsible for creating these rules - old_ports = [rule.get('port_range_max') + old_ports = [(rule.get('port_range_max'), rule.get('protocol')) for rule in rules.get('security_group_rules', []) # Don't remove egress rules and don't # confuse other protocols with None ports # with the egress rules. VRRP uses protocol # 51 and 112 if rule.get('direction') != 'egress' and - rule.get('protocol', '').lower() == 'tcp'] + rule.get('protocol', '').lower() in ['tcp', 'udp']] add_ports = set(updated_ports) - set(old_ports) del_ports = set(old_ports) - set(updated_ports) for rule in rules.get('security_group_rules', []): - if rule.get('port_range_max') in del_ports: + if (rule.get('protocol', '') and + rule.get('protocol', '').lower() in ['tcp', 'udp'] and + (rule.get('port_range_max'), + rule.get('protocol')) in del_ports): rule_id = rule.get('id') try: self.neutron_client.delete_security_group_rule(rule_id) @@ -167,9 +179,10 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): "it is already deleted.", rule_id) ethertype = self._get_ethertype_for_ip(load_balancer.vip.ip_address) - for port in add_ports: - self._create_security_group_rule(sec_grp_id, 'TCP', port_min=port, - port_max=port, + for port_protocol in add_ports: + self._create_security_group_rule(sec_grp_id, port_protocol[1], + port_min=port_protocol[0], + port_max=port_protocol[0], ethertype=ethertype) # Currently we are using the VIP network for VRRP 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 6151458ed4..91b35cb509 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 @@ -57,6 +57,9 @@ class TestServerTestCase(base.TestCase): 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.server.' + 'check_and_return_request_listener_protocol', + return_value='TCP').start() @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSTEMD) @@ -374,9 +377,13 @@ class TestServerTestCase(base.TestCase): def test_centos_info(self): self._test_info(consts.CENTOS) + @mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.' + 'AmphoraInfo._get_extend_body_from_udp_driver', + return_value={}) @mock.patch('socket.gethostname') @mock.patch('subprocess.check_output') - def _test_info(self, distro, mock_subbprocess, mock_hostname): + def _test_info(self, distro, mock_subbprocess, mock_hostname, + mock_get_extend_body): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) mock_hostname.side_effect = ['test-host'] mock_subbprocess.side_effect = ['9.9.99-9'] @@ -990,6 +997,7 @@ class TestServerTestCase(base.TestCase): agent_server_network_file="/path/to/interfaces_file") self._test_plug_network(consts.CENTOS) + @mock.patch('os.chmod') @mock.patch('netifaces.interfaces') @mock.patch('netifaces.ifaddresses') @mock.patch('pyroute2.IPRoute') @@ -1000,7 +1008,7 @@ class TestServerTestCase(base.TestCase): @mock.patch('os.path.isfile') def _test_plug_network(self, distro, mock_isfile, mock_int_exists, mock_check_output, mock_netns, mock_pyroute2, - mock_ifaddress, mock_interfaces): + mock_ifaddress, mock_interfaces, mock_os_chmod): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) port_info = {'mac_address': '123'} test_int_num = random.randint(0, 9999) @@ -1205,7 +1213,11 @@ class TestServerTestCase(base.TestCase): 'iface eth{int} inet static\n' 'address 10.0.0.5\nbroadcast 10.0.0.255\n' 'netmask 255.255.255.0\n' - 'mtu 1450\n'.format(int=test_int_num)) + 'mtu 1450\n' + 'post-up /sbin/iptables -t nat -A POSTROUTING -p udp ' + '-o eth{int} -j MASQUERADE\n' + 'post-down /sbin/iptables -t nat -D POSTROUTING -p udp ' + '-o eth{int} -j MASQUERADE\n'.format(int=test_int_num)) elif distro == consts.CENTOS: handle.write.assert_any_call( '\n\n# Generated by Octavia agent\n' @@ -1279,7 +1291,11 @@ class TestServerTestCase(base.TestCase): 'iface eth{int} 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\nmtu 1450\n'.format(int=test_int_num)) + 'netmask 32\nmtu 1450\n' + 'post-up /sbin/ip6tables -t nat -A POSTROUTING -p udp ' + '-o eth{int} -j MASQUERADE\n' + 'post-down /sbin/ip6tables -t nat -D POSTROUTING -p udp ' + '-o eth{int} -j MASQUERADE\n'.format(int=test_int_num)) elif distro == consts.CENTOS: handle.write.assert_any_call( '\n\n# Generated by Octavia agent\n' @@ -1384,6 +1400,7 @@ class TestServerTestCase(base.TestCase): def test_centos_plug_network_host_routes(self): self._test_plug_network_host_routes(consts.CENTOS) + @mock.patch('os.chmod') @mock.patch('netifaces.interfaces') @mock.patch('netifaces.ifaddresses') @mock.patch('pyroute2.IPRoute') @@ -1391,7 +1408,8 @@ class TestServerTestCase(base.TestCase): @mock.patch('subprocess.check_output') def _test_plug_network_host_routes(self, distro, mock_check_output, mock_netns, mock_pyroute2, - mock_ifaddress, mock_interfaces): + mock_ifaddress, mock_interfaces, + mock_os_chmod): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) @@ -1466,8 +1484,12 @@ class TestServerTestCase(base.TestCase): 'up route add -net ' + DEST2 + ' gw ' + NEXTHOP + ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' 'down route del -net ' + DEST2 + ' gw ' + NEXTHOP + - ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' - ) + ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' + + 'post-up /sbin/iptables -t nat -A POSTROUTING -p udp -o ' + + consts.NETNS_PRIMARY_INTERFACE + ' -j MASQUERADE' + '\n' + + 'post-down /sbin/iptables -t nat -D POSTROUTING -p udp ' + '-o ' + consts.NETNS_PRIMARY_INTERFACE + + ' -j MASQUERADE' + '\n') elif distro == consts.CENTOS: handle.write.assert_any_call( '\n\n# Generated by Octavia agent\n' @@ -1479,6 +1501,12 @@ class TestServerTestCase(base.TestCase): int=consts.NETNS_PRIMARY_INTERFACE, ip=IP, mask=NETMASK)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + mock_open.assert_any_call('/sbin/ifup-local', flags, mode) + mock_open.assert_any_call('/sbin/ifdown-local', flags, mode) + calls = [mock.call('/sbin/ifup-local', stat.S_IEXEC), + mock.call('/sbin/ifdown-local', stat.S_IEXEC)] + mock_os_chmod.assert_has_calls(calls) mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, 'ifup', consts.NETNS_PRIMARY_INTERFACE], stderr=-2) @@ -1494,6 +1522,7 @@ class TestServerTestCase(base.TestCase): agent_server_network_file="/path/to/interfaces_file") self._test_plug_VIP4(consts.CENTOS) + @mock.patch('os.chmod') @mock.patch('shutil.copy2') @mock.patch('pyroute2.NSPopen') @mock.patch('octavia.amphorae.backends.agent.api_server.' @@ -1511,7 +1540,7 @@ class TestServerTestCase(base.TestCase): mock_copytree, mock_check_output, mock_netns, mock_netns_create, mock_pyroute2, mock_ifaddress, mock_interfaces, mock_int_exists, mock_nspopen, - mock_copy2): + mock_copy2, mock_os_chmod): mock_isfile.return_value = True @@ -1717,7 +1746,11 @@ class TestServerTestCase(base.TestCase): '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 ' - 'priority 100'.format( + 'priority 100\n' + 'post-up /sbin/iptables -t nat -A POSTROUTING -p udp ' + '-o eth1 -j MASQUERADE\n' + 'post-down /sbin/iptables -t nat -D POSTROUTING -p udp ' + '-o eth1 -j MASQUERADE'.format( netns_int=consts.NETNS_PRIMARY_INTERFACE)) elif distro == consts.CENTOS: handle.write.assert_any_call( @@ -1728,15 +1761,29 @@ class TestServerTestCase(base.TestCase): 'NETMASK="255.255.255.0"\nGATEWAY="203.0.113.1"\n' 'MTU="1450" \n'.format( netns_int=consts.NETNS_PRIMARY_INTERFACE)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + mock_open.assert_any_call('/sbin/ifup-local', flags, mode) + mock_open.assert_any_call('/sbin/ifdown-local', flags, mode) + calls = [mock.call('/sbin/ifup-local', stat.S_IEXEC), + mock.call('/sbin/ifdown-local', stat.S_IEXEC)] + mock_os_chmod.assert_has_calls(calls) mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, 'ifup', '{netns_int}:0'.format( netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) # Verify sysctl was loaded - mock_nspopen.assert_called_once_with( - 'amphora-haproxy', ['/sbin/sysctl', '--system'], - stdout=subprocess.PIPE) + calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', ['modprobe', 'ip_vs'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', + ['/sbin/sysctl', '-w', 'net.ipv4.ip_forward=1'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', + ['/sbin/sysctl', '-w', 'net.ipv4.vs.conntrack=1'], + stdout=subprocess.PIPE)] + mock_nspopen.assert_has_calls(calls, any_order=True) # One Interface down, Happy Path IPv4 mock_interfaces.side_effect = [['blah']] @@ -1810,7 +1857,11 @@ class TestServerTestCase(base.TestCase): '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 ' - 'priority 100'.format( + 'priority 100\n' + 'post-up /sbin/iptables -t nat -A POSTROUTING -p udp ' + '-o eth1 -j MASQUERADE\n' + 'post-down /sbin/iptables -t nat -D POSTROUTING -p udp ' + '-o eth1 -j MASQUERADE'.format( netns_int=consts.NETNS_PRIMARY_INTERFACE)) elif distro == consts.CENTOS: handle.write.assert_any_call( @@ -1857,6 +1908,7 @@ class TestServerTestCase(base.TestCase): def test_centos_plug_VIP6(self): self._test_plug_vip6(consts.CENTOS) + @mock.patch('os.chmod') @mock.patch('shutil.copy2') @mock.patch('pyroute2.NSPopen') @mock.patch('netifaces.interfaces') @@ -1871,7 +1923,8 @@ class TestServerTestCase(base.TestCase): def _test_plug_vip6(self, distro, mock_isfile, mock_makedirs, mock_copytree, mock_check_output, mock_netns, mock_netns_create, mock_pyroute2, mock_ifaddress, - mock_interfaces, mock_nspopen, mock_copy2): + mock_interfaces, mock_nspopen, mock_copy2, + mock_os_chmod): mock_isfile.return_value = True @@ -2059,7 +2112,11 @@ class TestServerTestCase(base.TestCase): 'priority 100\n' 'post-down /sbin/ip -6 rule del from ' '2001:0db8:0000:0000:0000:0000:0000:0002/32 table 1 ' - 'priority 100'.format( + 'priority 100\n' + 'post-up /sbin/ip6tables -t nat -A POSTROUTING -p udp ' + '-o eth1 -j MASQUERADE\n' + 'post-down /sbin/ip6tables -t nat -D POSTROUTING -p udp ' + '-o eth1 -j MASQUERADE'.format( netns_int=consts.NETNS_PRIMARY_INTERFACE)) elif distro == consts.CENTOS: handle.write.assert_any_call( @@ -2085,9 +2142,18 @@ class TestServerTestCase(base.TestCase): netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) # Verify sysctl was loaded - mock_nspopen.assert_called_once_with( - 'amphora-haproxy', ['/sbin/sysctl', '--system'], - stdout=subprocess.PIPE) + calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', ['modprobe', 'ip_vs'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', + ['/sbin/sysctl', '-w', + 'net.ipv6.conf.all.forwarding=1'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', + ['/sbin/sysctl', '-w', 'net.ipv4.vs.conntrack=1'], + stdout=subprocess.PIPE)] + mock_nspopen.assert_has_calls(calls, any_order=True) # One Interface down, Happy Path IPv6 mock_interfaces.side_effect = [['blah']] @@ -2157,7 +2223,11 @@ class TestServerTestCase(base.TestCase): 'priority 100\n' 'post-down /sbin/ip -6 rule del from ' '2001:0db8:0000:0000:0000:0000:0000:0002/32 table 1 ' - 'priority 100'.format( + 'priority 100\n' + 'post-up /sbin/ip6tables -t nat -A POSTROUTING -p udp ' + '-o eth1 -j MASQUERADE\n' + 'post-down /sbin/ip6tables -t nat -D POSTROUTING -p udp ' + '-o eth1 -j MASQUERADE'.format( netns_int=consts.NETNS_PRIMARY_INTERFACE)) elif distro == consts.CENTOS: handle.write.assert_any_call( @@ -2401,6 +2471,19 @@ class TestServerTestCase(base.TestCase): def test_centos_details(self): self._test_details(consts.CENTOS) + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_udp_listeners', + return_value=[]) + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo.' + '_get_extend_body_from_udp_driver', + return_value={ + "keepalived_version": '1.1.11-1', + "ipvsadm_version": '2.2.22-2' + }) + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo.' + '_count_udp_listener_processes', return_value=0) @mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.' 'AmphoraInfo._count_haproxy_processes') @mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.' @@ -2419,7 +2502,8 @@ class TestServerTestCase(base.TestCase): def _test_details(self, distro, mock_subbprocess, mock_hostname, mock_get_listeners, mock_get_mem, mock_cpu, mock_statvfs, mock_load, mock_get_nets, - mock_count_haproxy): + mock_count_haproxy, mock_count_udp_listeners, + mock_get_ext_from_udp_driver, mock_get_udp_listeners): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) @@ -2514,6 +2598,8 @@ class TestServerTestCase(base.TestCase): 'haproxy_count': haproxy_count, 'haproxy_version': '9.9.99-9', 'hostname': 'test-host', + 'ipvsadm_version': u'2.2.22-2', + 'keepalived_version': u'1.1.11-1', 'listeners': [listener_id], 'load': [load_1min, load_5min, load_15min], 'memory': {'buffers': Buffers, @@ -2531,7 +2617,8 @@ class TestServerTestCase(base.TestCase): 'network_tx': eth3_tx}}, 'packages': {}, 'topology': consts.TOPOLOGY_SINGLE, - 'topology_status': consts.TOPOLOGY_STATUS_OK} + 'topology_status': consts.TOPOLOGY_STATUS_OK, + 'udp_listener_process_count': 0} if distro == consts.UBUNTU: rv = self.ubuntu_app.get('/' + api_server.VERSION + '/details') diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py index 4199f1d055..81fea3a4f9 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py @@ -27,11 +27,26 @@ class TestAmphoraInfo(base.TestCase): API_VERSION = random.randrange(0, 10000) HAPROXY_VERSION = random.randrange(0, 10000) + KEEPALIVED_VERSION = random.randrange(0, 10000) + IPVSADM_VERSION = random.randrange(0, 10000) + FAKE_LISTENER_ID_1 = uuidutils.generate_uuid() + FAKE_LISTENER_ID_2 = uuidutils.generate_uuid() + FAKE_LISTENER_ID_3 = uuidutils.generate_uuid() + FAKE_LISTENER_ID_4 = uuidutils.generate_uuid() def setUp(self): super(TestAmphoraInfo, self).setUp() self.osutils_mock = mock.MagicMock() self.amp_info = amphora_info.AmphoraInfo(self.osutils_mock) + self.udp_driver = mock.MagicMock() + + def _return_version(self, package_name): + if package_name == 'ipvsadm': + return self.IPVSADM_VERSION + elif package_name == 'keepalived': + return self.KEEPALIVED_VERSION + else: + return self.HAPROXY_VERSION @mock.patch.object(amphora_info, "webob") @mock.patch('octavia.amphorae.backends.agent.api_server.' @@ -49,6 +64,186 @@ class TestAmphoraInfo(base.TestCase): mock_webob.Response.assert_called_once_with(json=expected_dict) api_server.VERSION = original_version + @mock.patch.object(amphora_info, "webob") + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._get_version_of_installed_package') + @mock.patch('socket.gethostname', return_value='FAKE_HOST') + def test_compile_amphora_info_for_udp(self, mock_gethostname, + mock_pkg_version, mock_webob): + + mock_pkg_version.side_effect = self._return_version + self.udp_driver.get_subscribed_amp_compile_info.side_effect = [ + ['keepalived', 'ipvsadm']] + original_version = api_server.VERSION + api_server.VERSION = self.API_VERSION + expected_dict = {'api_version': self.API_VERSION, + 'hostname': 'FAKE_HOST', + 'haproxy_version': self.HAPROXY_VERSION, + 'keepalived_version': self.KEEPALIVED_VERSION, + 'ipvsadm_version': self.IPVSADM_VERSION + } + self.amp_info.compile_amphora_info(extend_udp_driver=self.udp_driver) + mock_webob.Response.assert_called_once_with(json=expected_dict) + api_server.VERSION = original_version + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_listeners', return_value=[FAKE_LISTENER_ID_1, + FAKE_LISTENER_ID_2]) + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._get_meminfo') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._cpu') + @mock.patch('os.statvfs') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._get_networks') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._load') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._get_version_of_installed_package') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._count_haproxy_processes') + @mock.patch('socket.gethostname', return_value='FAKE_HOST') + def test_compile_amphora_details(self, mhostname, m_count, m_pkg_version, + m_load, m_get_nets, m_os, m_cpu, + mget_mem, mget_listener): + mget_mem.return_value = {'SwapCached': 0, 'Buffers': 344792, + 'MemTotal': 21692784, 'Cached': 4271856, + 'Slab': 534384, 'MemFree': 12685624, + 'Shmem': 9520} + m_cpu.return_value = {'user': '252551', 'softirq': '8336', + 'system': '52554', 'total': 7503411} + m_pkg_version.side_effect = self._return_version + mdisk_info = mock.MagicMock() + m_os.return_value = mdisk_info + mdisk_info.f_blocks = 34676992 + mdisk_info.f_bfree = 28398016 + mdisk_info.f_frsize = 4096 + mdisk_info.f_bavail = 26630646 + m_get_nets.return_value = {'eth1': {'network_rx': 996, + 'network_tx': 418}, + 'eth2': {'network_rx': 848, + 'network_tx': 578}} + m_load.return_value = ['0.09', '0.11', '0.10'] + m_count.return_value = 5 + original_version = api_server.VERSION + api_server.VERSION = self.API_VERSION + expected_dict = {u'active': True, + u'api_version': self.API_VERSION, + u'cpu': {u'soft_irq': u'8336', + u'system': u'52554', + u'total': 7503411, + u'user': u'252551'}, + u'disk': {u'available': 109079126016, + u'used': 25718685696}, + u'haproxy_count': 5, + u'haproxy_version': self.HAPROXY_VERSION, + u'hostname': u'FAKE_HOST', + u'listeners': [self.FAKE_LISTENER_ID_1, + self.FAKE_LISTENER_ID_2], + u'load': [u'0.09', u'0.11', u'0.10'], + u'memory': {u'buffers': 344792, + u'cached': 4271856, + u'free': 12685624, + u'shared': 9520, + u'slab': 534384, + u'swap_used': 0, + u'total': 21692784}, + u'networks': {u'eth1': {u'network_rx': 996, + u'network_tx': 418}, + u'eth2': {u'network_rx': 848, + u'network_tx': 578}}, + u'packages': {}, + u'topology': u'SINGLE', + u'topology_status': u'OK'} + actual = self.amp_info.compile_amphora_details() + self.assertEqual(expected_dict, actual.json) + api_server.VERSION = original_version + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_udp_listeners', + return_value=[FAKE_LISTENER_ID_3, FAKE_LISTENER_ID_4]) + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_listeners', return_value=[FAKE_LISTENER_ID_1, + FAKE_LISTENER_ID_2]) + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._get_meminfo') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._cpu') + @mock.patch('os.statvfs') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._get_networks') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._load') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._get_version_of_installed_package') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._count_haproxy_processes') + @mock.patch('socket.gethostname', return_value='FAKE_HOST') + def test_compile_amphora_details_for_udp(self, mhostname, m_count, + m_pkg_version, m_load, m_get_nets, + m_os, m_cpu, mget_mem, + mget_listener, mget_udp_listener): + mget_mem.return_value = {'SwapCached': 0, 'Buffers': 344792, + 'MemTotal': 21692784, 'Cached': 4271856, + 'Slab': 534384, 'MemFree': 12685624, + 'Shmem': 9520} + m_cpu.return_value = {'user': '252551', 'softirq': '8336', + 'system': '52554', 'total': 7503411} + m_pkg_version.side_effect = self._return_version + mdisk_info = mock.MagicMock() + m_os.return_value = mdisk_info + mdisk_info.f_blocks = 34676992 + mdisk_info.f_bfree = 28398016 + mdisk_info.f_frsize = 4096 + mdisk_info.f_bavail = 26630646 + m_get_nets.return_value = {'eth1': {'network_rx': 996, + 'network_tx': 418}, + 'eth2': {'network_rx': 848, + 'network_tx': 578}} + m_load.return_value = ['0.09', '0.11', '0.10'] + m_count.return_value = 5 + self.udp_driver.get_subscribed_amp_compile_info.return_value = [ + 'keepalived', 'ipvsadm'] + self.udp_driver.is_listener_running.side_effect = [True, False] + original_version = api_server.VERSION + api_server.VERSION = self.API_VERSION + expected_dict = {u'active': True, + u'api_version': self.API_VERSION, + u'cpu': {u'soft_irq': u'8336', + u'system': u'52554', + u'total': 7503411, + u'user': u'252551'}, + u'disk': {u'available': 109079126016, + u'used': 25718685696}, + u'haproxy_count': 5, + u'haproxy_version': self.HAPROXY_VERSION, + u'keepalived_version': self.KEEPALIVED_VERSION, + u'ipvsadm_version': self.IPVSADM_VERSION, + u'udp_listener_process_count': 1, + u'hostname': u'FAKE_HOST', + u'listeners': list(set([self.FAKE_LISTENER_ID_1, + self.FAKE_LISTENER_ID_2, + self.FAKE_LISTENER_ID_3, + self.FAKE_LISTENER_ID_4])), + u'load': [u'0.09', u'0.11', u'0.10'], + u'memory': {u'buffers': 344792, + u'cached': 4271856, + u'free': 12685624, + u'shared': 9520, + u'slab': 534384, + u'swap_used': 0, + u'total': 21692784}, + u'networks': {u'eth1': {u'network_rx': 996, + u'network_tx': 418}, + u'eth2': {u'network_rx': 848, + u'network_tx': 578}}, + u'packages': {}, + u'topology': u'SINGLE', + u'topology_status': u'OK'} + actual = self.amp_info.compile_amphora_details(self.udp_driver) + self.assertEqual(expected_dict, actual.json) + api_server.VERSION = original_version + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'is_listener_running') def test__count_haproxy_process(self, mock_is_running): @@ -63,6 +258,29 @@ class TestAmphoraInfo(base.TestCase): [uuidutils.generate_uuid(), uuidutils.generate_uuid()]) self.assertEqual(1, result) + def test__count_udp_listener_processes(self): + self.udp_driver.is_listener_running.side_effect = [True, False, True] + expected = 2 + actual = self.amp_info._count_udp_listener_processes( + self.udp_driver, [self.FAKE_LISTENER_ID_1, + self.FAKE_LISTENER_ID_2, + self.FAKE_LISTENER_ID_3]) + self.assertEqual(expected, actual) + + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._get_version_of_installed_package') + def test__get_extend_body_from_udp_driver(self, m_get_version): + self.udp_driver.get_subscribed_amp_compile_info.return_value = [ + 'keepalived', 'ipvsadm'] + m_get_version.side_effect = self._return_version + expected = { + "keepalived_version": self.KEEPALIVED_VERSION, + "ipvsadm_version": self.IPVSADM_VERSION + } + actual = self.amp_info._get_extend_body_from_udp_driver( + self.udp_driver) + self.assertEqual(expected, actual) + def test__get_meminfo(self): # Known data test meminfo = ('MemTotal: 21692784 kB\n' diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_keepalivedlvs.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_keepalivedlvs.py new file mode 100644 index 0000000000..282c727545 --- /dev/null +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_keepalivedlvs.py @@ -0,0 +1,422 @@ +# Copyright 2015 Hewlett Packard Enterprise Development Company LP +# +# 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 os +import stat +import subprocess + +import flask +import mock +from werkzeug import exceptions + +from oslo_utils import uuidutils + +from octavia.amphorae.backends.agent.api_server import keepalivedlvs +from octavia.amphorae.backends.agent.api_server import server +from octavia.amphorae.backends.agent.api_server import util +from octavia.common import constants as consts +from octavia.tests.common import utils as test_utils +from octavia.tests.unit import base + + +class KeepalivedLvsTestCase(base.TestCase): + FAKE_ID = uuidutils.generate_uuid() + LISTENER_ID = 'listener-1111-1111-1111-listenerid00' + POOL_ID = 'poolpool-1111-1111-1111-poolid000000' + MEMBER_ID1 = 'memberid-1111-1111-1111-memberid1111' + MEMBER_ID2 = 'memberid-2222-2222-2222-memberid2222' + HEALTHMONITOR_ID = 'hmidhmid-1111-1111-1111-healthmonito' + NORMAL_CFG_CONTENT = ( + "# Configuration for Listener %(listener_id)s\n\n" + "net_namespace haproxy-amphora\n\n" + "virtual_server 10.0.0.2 80 {\n" + " lb_algo rr\n" + " lb_kind NAT\n" + " protocol udp\n" + " delay_loop 30\n" + " delay_before_retry 31\n" + " retry 3\n\n\n" + " # Configuration for Pool %(pool_id)s\n" + " # Configuration for HealthMonitor %(hm_id)s\n" + " # Configuration for Member %(member1_id)s\n" + " real_server 10.0.0.99 82 {\n" + " weight 13\n" + " inhibit_on_failure\n" + " uthreshold 98\n" + " persistence_timeout 33\n" + " persistence_granularity 255.255.0.0\n" + " delay_before_retry 31\n" + " retry 3\n" + " MISC_CHECK {\n" + " misc_path \"/var/lib/octavia/lvs/check/" + "udp_check.sh 10.0.0.99 82\"\n" + " misc_timeout 30\n" + " misc_dynamic\n" + " }\n" + " }\n\n" + " # Configuration for Member %(member2_id)s\n" + " real_server 10.0.0.98 82 {\n" + " weight 13\n" + " inhibit_on_failure\n" + " uthreshold 98\n" + " persistence_timeout 33\n" + " persistence_granularity 255.255.0.0\n" + " delay_before_retry 31\n" + " retry 3\n" + " MISC_CHECK {\n" + " misc_path \"/var/lib/octavia/lvs/check/" + "udp_check.sh 10.0.0.98 82\"\n" + " misc_timeout 30\n" + " misc_dynamic\n" + " }\n" + " }\n\n" + "}\n\n") % {'listener_id': LISTENER_ID, 'pool_id': POOL_ID, + 'hm_id': HEALTHMONITOR_ID, 'member1_id': MEMBER_ID1, + 'member2_id': MEMBER_ID2} + PROC_CONTENT = ( + "IP Virtual Server version 1.2.1 (size=4096)\n" + "Prot LocalAddress:Port Scheduler Flags\n" + " -> RemoteAddress:Port Forward Weight ActiveConn InActConn\n" + "UDP 0A000002:0050 sh\n" + " -> 0A000063:0052 Masq 13 1 0\n" + " -> 0A000062:0052 Masq 13 1 0\n" + ) + NORMAL_PID_CONTENT = "1988" + TEST_URL = server.PATH_PREFIX + '/listeners/%s/%s/udp_listener' + + def setUp(self): + super(KeepalivedLvsTestCase, self).setUp() + self.app = flask.Flask(__name__) + self.client = self.app.test_client() + self._ctx = self.app.test_request_context() + self._ctx.push() + self.test_keepalivedlvs = keepalivedlvs.KeepalivedLvs() + self.app.add_url_rule( + rule=self.TEST_URL % ('', ''), + view_func=(lambda amphora_id, listener_id: + self.test_keepalivedlvs.upload_udp_listener_config( + listener_id)), + methods=['PUT']) + + @mock.patch('pyroute2.NetNS') + @mock.patch('shutil.copy2') + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_os_init_system', return_value=consts.INIT_SYSTEMD) + @mock.patch('os.chmod') + @mock.patch('os.path.exists') + @mock.patch('os.makedirs') + @mock.patch('os.remove') + @mock.patch('subprocess.check_output') + def test_upload_udp_listener_config_no_vrrp_check_dir( + self, m_check_output, m_os_rm, m_os_mkdir, m_exists, m_os_chmod, + m_os_sysinit, m_copy2, mock_netns): + m_exists.side_effect = [False, False, True, True, False, False] + cfg_path = util.keepalived_lvs_cfg_path(self.FAKE_ID) + m = self.useFixture(test_utils.OpenFixture(cfg_path)).mock_open + + with mock.patch('os.open') as m_open, mock.patch.object(os, + 'fdopen', + m) as m_fdopen: + m_open.side_effect = ['TEST-WRITE-CFG', + 'TEST-WRITE-SYSINIT'] + res = self.client.put(self.TEST_URL % ('123', self.FAKE_ID), + data=self.NORMAL_CFG_CONTENT) + os_mkdir_calls = [ + mock.call(util.keepalived_lvs_dir()), + mock.call(util.keepalived_backend_check_script_dir()) + ] + m_os_mkdir.assert_has_calls(os_mkdir_calls) + + m_os_chmod.assert_called_with( + util.keepalived_backend_check_script_path(), stat.S_IEXEC) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + systemd_cfg_path = util.keepalived_lvs_init_path( + consts.INIT_SYSTEMD, self.FAKE_ID) + m_open_calls = [ + mock.call(cfg_path, flags, mode), + mock.call(systemd_cfg_path, flags, mode) + ] + m_open.assert_has_calls(m_open_calls) + m_fdopen.assert_any_call('TEST-WRITE-CFG', 'wb') + m_fdopen.assert_any_call('TEST-WRITE-SYSINIT', 'w') + self.assertEqual(200, res.status_code) + + @mock.patch('pyroute2.NetNS') + @mock.patch('shutil.copy2') + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_os_init_system', return_value=consts.INIT_SYSTEMD) + @mock.patch('os.chmod') + @mock.patch('os.path.exists') + @mock.patch('os.makedirs') + @mock.patch('os.remove') + @mock.patch('subprocess.check_output') + def test_upload_udp_listener_config_with_vrrp_check_dir( + self, m_check_output, m_os_rm, m_os_mkdir, m_exists, m_os_chmod, + m_os_sysinit, m_copy2, mock_netns): + m_exists.side_effect = [False, False, True, True, True, False, False] + cfg_path = util.keepalived_lvs_cfg_path(self.FAKE_ID) + m = self.useFixture(test_utils.OpenFixture(cfg_path)).mock_open + + with mock.patch('os.open') as m_open, mock.patch.object(os, + 'fdopen', + m) as m_fdopen: + m_open.side_effect = ['TEST-WRITE-CFG', + 'TEST-WRITE-SYSINIT', + 'TEST-WRITE-UDP-VRRP-CHECK'] + res = self.client.put(self.TEST_URL % ('123', self.FAKE_ID), + data=self.NORMAL_CFG_CONTENT) + os_mkdir_calls = [ + mock.call(util.keepalived_lvs_dir()), + mock.call(util.keepalived_backend_check_script_dir()) + ] + m_os_mkdir.assert_has_calls(os_mkdir_calls) + + m_os_chmod.assert_called_with( + util.keepalived_backend_check_script_path(), stat.S_IEXEC) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + systemd_cfg_path = util.keepalived_lvs_init_path( + consts.INIT_SYSTEMD, self.FAKE_ID) + script_path = os.path.join( + util.keepalived_check_scripts_dir(), + keepalivedlvs.KEEPALIVED_CHECK_SCRIPT_NAME) + m_open_calls = [ + mock.call(cfg_path, flags, mode), + mock.call(systemd_cfg_path, flags, mode), + mock.call(script_path, flags, stat.S_IEXEC) + ] + m_open.assert_has_calls(m_open_calls) + m_fdopen.assert_any_call('TEST-WRITE-CFG', 'wb') + m_fdopen.assert_any_call('TEST-WRITE-SYSINIT', 'w') + m_fdopen.assert_any_call('TEST-WRITE-UDP-VRRP-CHECK', 'w') + self.assertEqual(200, res.status_code) + + @mock.patch('shutil.copy2') + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_os_init_system', return_value=consts.INIT_SYSTEMD) + @mock.patch('os.chmod') + @mock.patch('os.path.exists') + @mock.patch('os.makedirs') + @mock.patch('os.remove') + @mock.patch('subprocess.check_output') + def test_upload_udp_listener_config_start_service_failure( + self, m_check_output, m_os_rm, m_os_mkdir, m_exists, m_os_chmod, + m_os_sysinit, m_copy2): + m_exists.side_effect = [False, False, True, True, True, False] + m_check_output.side_effect = subprocess.CalledProcessError(1, 'blah!') + cfg_path = util.keepalived_lvs_cfg_path(self.FAKE_ID) + m = self.useFixture(test_utils.OpenFixture(cfg_path)).mock_open + + with mock.patch('os.open') as m_open, mock.patch.object(os, + 'fdopen', + m) as m_fdopen: + m_open.side_effect = ['TEST-WRITE-CFG', + 'TEST-WRITE-SYSINIT'] + res = self.client.put(self.TEST_URL % ('123', self.FAKE_ID), + data=self.NORMAL_CFG_CONTENT) + os_mkdir_calls = [ + mock.call(util.keepalived_lvs_dir()), + mock.call(util.keepalived_backend_check_script_dir()) + ] + m_os_mkdir.assert_has_calls(os_mkdir_calls) + + m_os_chmod.assert_called_with( + util.keepalived_backend_check_script_path(), stat.S_IEXEC) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + systemd_cfg_path = util.keepalived_lvs_init_path( + consts.INIT_SYSTEMD, self.FAKE_ID) + m_open_calls = [ + mock.call(cfg_path, flags, mode), + mock.call(systemd_cfg_path, flags, mode) + ] + m_open.assert_has_calls(m_open_calls) + m_fdopen.assert_any_call('TEST-WRITE-CFG', 'wb') + m_fdopen.assert_any_call('TEST-WRITE-SYSINIT', 'w') + self.assertEqual(500, res.status_code) + + @mock.patch('subprocess.check_output') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'keepalivedlvs.KeepalivedLvs.' + '_check_udp_listener_exists') + def test_manage_udp_listener(self, mock_udp_exist, mock_check_output): + res = self.test_keepalivedlvs.manage_udp_listener(self.FAKE_ID, + 'start') + cmd = ("/usr/sbin/service octavia-keepalivedlvs-{listener_id}" + " {action}".format(listener_id=self.FAKE_ID, action='start')) + mock_check_output.assert_called_once_with(cmd.split(), + stderr=subprocess.STDOUT) + self.assertEqual(202, res.status_code) + + res = self.test_keepalivedlvs.manage_udp_listener(self.FAKE_ID, + 'restart') + self.assertEqual(400, res.status_code) + + mock_check_output.side_effect = subprocess.CalledProcessError(1, + 'blah!') + + res = self.test_keepalivedlvs.manage_udp_listener(self.FAKE_ID, + 'start') + self.assertEqual(500, res.status_code) + + @mock.patch('octavia.amphorae.backends.utils.keepalivedlvs_query.' + 'get_listener_realserver_mapping') + @mock.patch('subprocess.check_output', return_value=PROC_CONTENT) + @mock.patch('os.path.exists') + def test_get_udp_listener_status(self, m_exist, m_check_output, + mget_mapping): + mget_mapping.return_value = ( + True, {'10.0.0.99:82': {'status': 'UP', + 'Weight': '13', + 'InActConn': '0', + 'ActiveConn': '0'}, + '10.0.0.98:82': {'status': 'UP', + 'Weight': '13', + 'InActConn': '0', + 'ActiveConn': '0'}}) + pid_path = ('/var/lib/octavia/lvs/octavia-' + 'keepalivedlvs-%s.pid' % self.FAKE_ID) + self.useFixture(test_utils.OpenFixture(pid_path, + self.NORMAL_PID_CONTENT)) + + cfg_path = ('/var/lib/octavia/lvs/octavia-' + 'keepalivedlvs-%s.conf' % self.FAKE_ID) + self.useFixture(test_utils.OpenFixture(cfg_path, + self.NORMAL_CFG_CONTENT)) + + m_exist.return_value = True + expected = {'status': 'ACTIVE', + 'pools': [{'lvs': { + 'members': {self.MEMBER_ID1: 'UP', + self.MEMBER_ID2: 'UP'}, + 'status': 'UP', + 'uuid': self.POOL_ID}}], + 'type': 'lvs', 'uuid': self.FAKE_ID} + res = self.test_keepalivedlvs.get_udp_listener_status(self.FAKE_ID) + self.assertEqual(200, res.status_code) + self.assertEqual(expected, res.json) + + @mock.patch('os.path.exists') + def test_get_udp_listener_status_no_exists(self, m_exist): + m_exist.return_value = False + self.assertRaises(exceptions.HTTPException, + self.test_keepalivedlvs.get_udp_listener_status, + self.FAKE_ID) + + @mock.patch('os.path.exists') + def test_get_udp_listener_status_offline_status(self, m_exist): + m_exist.return_value = True + pid_path = ('/var/lib/octavia/lvs/octavia-' + 'keepalivedlvs-%s.pid' % self.FAKE_ID) + self.useFixture(test_utils.OpenFixture(pid_path, + self.NORMAL_PID_CONTENT)) + cfg_path = ('/var/lib/octavia/lvs/octavia-' + 'keepalivedlvs-%s.conf' % self.FAKE_ID) + self.useFixture(test_utils.OpenFixture(cfg_path, 'NO VS CONFIG')) + expected = {'status': 'OFFLINE', + 'type': '', + 'uuid': self.FAKE_ID} + res = self.test_keepalivedlvs.get_udp_listener_status(self.FAKE_ID) + self.assertEqual(200, res.status_code) + self.assertEqual(expected, res.json) + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_udp_listeners', return_value=[LISTENER_ID]) + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_os_init_system', return_value=consts.INIT_SYSTEMD) + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_keepalivedlvs_pid') + @mock.patch('subprocess.check_output') + @mock.patch('os.remove') + @mock.patch('os.path.exists') + def test_delete_udp_listener(self, m_exist, m_remove, m_check_output, + mget_pid, m_init_sys, mget_udp_listeners): + m_exist.return_value = True + res = self.test_keepalivedlvs.delete_udp_listener(self.FAKE_ID) + + cmd1 = ("/usr/sbin/service " + "octavia-keepalivedlvs-{0} stop".format(self.FAKE_ID)) + cmd2 = ("systemctl disable " + "octavia-keepalivedlvs-{list}".format(list=self.FAKE_ID)) + calls = [ + mock.call(cmd1.split(), stderr=subprocess.STDOUT), + mock.call(cmd2.split(), stderr=subprocess.STDOUT) + ] + m_check_output.assert_has_calls(calls) + self.assertEqual(200, res.status_code) + + @mock.patch.object(keepalivedlvs, "webob") + @mock.patch('os.path.exists') + def test_delete_udp_listener_not_exist(self, m_exist, m_webob): + m_exist.return_value = False + self.test_keepalivedlvs.delete_udp_listener(self.FAKE_ID) + calls = [ + mock.call( + json=dict(message='UDP Listener Not Found', + details="No UDP listener with UUID: " + "{0}".format(self.FAKE_ID)), status=404), + mock.call(json={'message': 'OK'}) + ] + m_webob.Response.assert_has_calls(calls) + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_keepalivedlvs_pid') + @mock.patch('subprocess.check_output') + @mock.patch('os.path.exists') + def test_delete_udp_listener_stop_service_fail(self, m_exist, + m_check_output, mget_pid): + m_exist.return_value = True + m_check_output.side_effect = subprocess.CalledProcessError(1, + 'Woops!') + res = self.test_keepalivedlvs.delete_udp_listener(self.FAKE_ID) + self.assertEqual(500, res.status_code) + self.assertEqual({'message': 'Error stopping keepalivedlvs', + 'details': None}, res.json) + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_os_init_system', return_value=consts.INIT_SYSVINIT) + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_keepalivedlvs_pid') + @mock.patch('subprocess.check_output') + @mock.patch('os.remove') + @mock.patch('os.path.exists') + def test_delete_udp_listener_disable_service_fail(self, m_exist, m_remove, + m_check_output, mget_pid, + m_init_sys): + m_exist.return_value = True + m_check_output.side_effect = [True, + subprocess.CalledProcessError( + 1, 'Woops!')] + res = self.test_keepalivedlvs.delete_udp_listener(self.FAKE_ID) + self.assertEqual(500, res.status_code) + self.assertEqual({ + 'message': 'Error disabling ' + 'octavia-keepalivedlvs-%s service' % self.FAKE_ID, + 'details': None}, res.json) + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_os_init_system') + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_keepalivedlvs_pid') + @mock.patch('subprocess.check_output') + @mock.patch('os.remove') + @mock.patch('os.path.exists') + def test_delete_udp_listener_unsupported_sysinit(self, m_exist, m_remove, + m_check_output, mget_pid, + m_init_sys): + m_exist.return_value = True + self.assertRaises( + util.UnknownInitError, self.test_keepalivedlvs.delete_udp_listener, + self.FAKE_ID) 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 648de7fd8e..70407092bf 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 @@ -88,9 +88,17 @@ class TestPlug(base.TestCase): 'details': 'VIP {vip} plugged on interface {interface}'.format( vip=FAKE_IP_IPV4, interface='eth1') }, status=202) - mock_nspopen.assert_called_once_with( - 'amphora-haproxy', ['/sbin/sysctl', '--system'], - stdout=subprocess.PIPE) + calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', ['modprobe', 'ip_vs'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', + ['/sbin/sysctl', '-w', 'net.ipv4.ip_forward=1'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', + ['/sbin/sysctl', '-w', 'net.ipv4.vs.conntrack=1'], + stdout=subprocess.PIPE)] + mock_nspopen.assert_has_calls(calls, any_order=True) @mock.patch('pyroute2.NSPopen') @mock.patch.object(plug, "webob") @@ -116,9 +124,18 @@ class TestPlug(base.TestCase): 'details': 'VIP {vip} plugged on interface {interface}'.format( vip=FAKE_IP_IPV6_EXPANDED, interface='eth1') }, status=202) - mock_nspopen.assert_called_once_with( - 'amphora-haproxy', ['/sbin/sysctl', '--system'], - stdout=subprocess.PIPE) + calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', ['modprobe', 'ip_vs'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', + ['/sbin/sysctl', '-w', + 'net.ipv6.conf.all.forwarding=1'], + stdout=subprocess.PIPE), + mock.call('amphora-haproxy', + ['/sbin/sysctl', '-w', 'net.ipv4.vs.conntrack=1'], + stdout=subprocess.PIPE)] + mock_nspopen.assert_has_calls(calls, any_order=True) @mock.patch.object(plug, "webob") @mock.patch('pyroute2.IPRoute') @@ -191,7 +208,11 @@ class TestPlugNetwork(base.TestCase): 'up route add -net {dest1} gw {nexthop} dev {netns_interface}\n' 'down route del -net {dest1} gw {nexthop} dev {netns_interface}\n' 'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n' - 'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n') + 'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n' + 'post-up /sbin/iptables -t nat -A POSTROUTING -p udp -o ' + 'eth1234 -j MASQUERADE\n' + 'post-down /sbin/iptables -t nat -D POSTROUTING -p udp -o eth1234 ' + '-j MASQUERADE\n') template_port = osutils.j2_env.get_template('plug_port_ethX.conf.j2') text = self.test_plug._osutils._generate_network_file_text( 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..1fd2927153 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 @@ -34,6 +34,8 @@ class AgentJinjaTestCase(base.TestCase): agent_server_cert='/etc/octavia/certs/server.pem') self.conf.config(group="amphora_agent", agent_server_network_dir='/etc/network/interfaces.d/') + self.conf.config(group='amphora_agent', + amphora_udp_driver='keepalived_lvs'), self.conf.config(group="haproxy_amphora", base_cert_dir='/var/lib/octavia/certs') self.conf.config(group="haproxy_amphora", use_upstart='True') @@ -79,7 +81,8 @@ 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' + 'amphora_udp_driver = keepalived_lvs') agent_cfg = ajc.build_agent_config(AMP_ID) self.assertEqual(expected_config, agent_cfg) @@ -114,6 +117,42 @@ 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' + 'amphora_udp_driver = keepalived_lvs') + agent_cfg = ajc.build_agent_config(AMP_ID) + self.assertEqual(expected_config, agent_cfg) + + def test_build_agent_config_with_new_udp_driver(self): + ajc = agent_jinja_cfg.AgentJinjaTemplater() + self.conf.config(group='amphora_agent', + agent_server_network_file=None) + self.conf.config(group="amphora_agent", + amphora_udp_driver='new_udp_driver') + expected_config = ('\n[DEFAULT]\n' + 'debug = False\n\n' + '[haproxy_amphora]\n' + 'base_cert_dir = /var/lib/octavia/certs\n' + 'base_path = /var/lib/octavia\n' + 'bind_host = 0.0.0.0\n' + 'bind_port = 9443\n' + 'haproxy_cmd = /usr/sbin/haproxy\n' + 'respawn_count = 2\n' + 'respawn_interval = 2\n' + 'use_upstart = True\n' + 'user_group = nogroup\n\n' + '[health_manager]\n' + 'controller_ip_port_list = 192.0.2.10:5555\n' + 'heartbeat_interval = 10\n' + 'heartbeat_key = TEST\n\n' + '[amphora_agent]\n' + 'agent_server_ca = ' + '/etc/octavia/certs/client_ca.pem\n' + 'agent_server_cert = ' + '/etc/octavia/certs/server.pem\n' + 'agent_server_network_dir = ' + '/etc/network/interfaces.d/\n' + 'agent_request_read_timeout = 120\n' + 'amphora_id = ' + AMP_ID + '\n' + 'amphora_udp_driver = new_udp_driver') agent_cfg = ajc.build_agent_config(AMP_ID) self.assertEqual(expected_config, agent_cfg) diff --git a/octavia/tests/unit/amphorae/backends/health_daemon/test_health_daemon.py b/octavia/tests/unit/amphorae/backends/health_daemon/test_health_daemon.py index 00a5fb4427..ac60981728 100644 --- a/octavia/tests/unit/amphorae/backends/health_daemon/test_health_daemon.py +++ b/octavia/tests/unit/amphorae/backends/health_daemon/test_health_daemon.py @@ -20,6 +20,7 @@ from oslo_utils import uuidutils import six from octavia.amphorae.backends.health_daemon import health_daemon +from octavia.common import constants import octavia.tests.unit.base as base if six.PY2: @@ -353,6 +354,65 @@ class TestHealthDaemon(base.TestCase): self.assertEqual(msg['listeners'][LISTENER_ID1]['pools'], {}) + @mock.patch("octavia.amphorae.backends.utils.keepalivedlvs_query." + "get_udp_listener_pool_status") + @mock.patch("octavia.amphorae.backends.utils.keepalivedlvs_query." + "get_udp_listeners_stats") + @mock.patch("octavia.amphorae.backends.agent.api_server.util." + "get_udp_listeners") + def test_bulid_stats_message_with_udp_listener( + self, mock_get_udp_listeners, mock_get_listener_stats, + mock_get_pool_status): + udp_listener_id1 = uuidutils.generate_uuid() + udp_listener_id2 = uuidutils.generate_uuid() + udp_listener_id3 = uuidutils.generate_uuid() + pool_id = uuidutils.generate_uuid() + member_id1 = uuidutils.generate_uuid() + member_id2 = uuidutils.generate_uuid() + mock_get_udp_listeners.return_value = [udp_listener_id1, + udp_listener_id2, + udp_listener_id3] + mock_get_listener_stats.return_value = { + udp_listener_id1: { + 'status': constants.OPEN, + 'stats': {'bin': 6387472, 'stot': 5, 'bout': 7490, + 'ereq': 0, 'scur': 0}}, + udp_listener_id3: { + 'status': constants.DOWN, + 'stats': {'bin': 0, 'stot': 0, 'bout': 0, + 'ereq': 0, 'scur': 0}} + } + udp_pool_status = { + 'lvs': { + 'uuid': pool_id, + 'status': constants.UP, + 'members': {member_id1: constants.UP, + member_id2: constants.UP}}} + mock_get_pool_status.side_effect = ( + lambda x: udp_pool_status if x == udp_listener_id1 else {}) + # the first listener can get all necessary info. + # the second listener can not get listener stats, so we won't report it + # the third listener can get listener stats, but can not get pool + # status, so the result will just contain the listener status for it. + expected = { + 'listeners': { + udp_listener_id1: { + 'status': constants.OPEN, + 'pools': { + pool_id: { + 'status': constants.UP, + 'members': { + member_id1: constants.UP, + member_id2: constants.UP}}}, + 'stats': {'conns': 0, 'totconns': 5, 'ereq': 0, + 'rx': 6387472, 'tx': 7490}}, + udp_listener_id3: { + 'status': constants.DOWN, + 'stats': {'conns': 0, 'totconns': 0, 'ereq': 0, + 'rx': 0, 'tx': 0}}}, 'id': None, 'seq': mock.ANY} + msg = health_daemon.build_stats_message() + self.assertEqual(expected, msg) + class FileNotFoundError(IOError): errno = 2 diff --git a/octavia/tests/unit/amphorae/backends/utils/test_keepalivedlvs_query.py b/octavia/tests/unit/amphorae/backends/utils/test_keepalivedlvs_query.py new file mode 100644 index 0000000000..78a382b6bb --- /dev/null +++ b/octavia/tests/unit/amphorae/backends/utils/test_keepalivedlvs_query.py @@ -0,0 +1,397 @@ +# 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 mock +from oslo_utils import uuidutils + +from octavia.amphorae.backends.agent.api_server import util +from octavia.amphorae.backends.utils import keepalivedlvs_query as lvs_query +from octavia.common import constants +from octavia.tests.common import utils as test_utils +from octavia.tests.unit import base + +# Kernal_file_sample which is in /proc/net/ip_vs +# The realservers and the listened ports are +# 10.0.0.25:2222, 10.0.0.35:3333. +# The virtual server and the listened port is +# 10.0.0.37:7777. +KERNAL_FILE_SAMPLE_V4 = ( + "IP Virtual Server version 1.2.1 (size=4096)\n" + "Prot LocalAddress:Port Scheduler Flags\n" + " -> RemoteAddress:Port Forward Weight ActiveConn InActConn\n" + "UDP 0A000025:1E61 rr\n" + " -> 0A000023:0D05 Masq 2 0 0\n" + " -> 0A000019:08AE Masq 3 0 0") +# Kernal_file_sample which is in /proc/net/ip_vs +# The realservers and the listened ports are +# [fd79:35e2:9963:0:f816:3eff:feca:b7bf]:2222, +# [fd79:35e2:9963:0:f816:3eff:fe9d:94df]:3333. +# The virtual server and the listened port is +# [fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7777. +KERNAL_FILE_SAMPLE_V6 = ( + "IP Virtual Server version 1.2.1 (size=4096)\n" + "Prot LocalAddress:Port Scheduler Flags\n" + " -> RemoteAddress:Port Forward Weight ActiveConn InActConn\n" + "UDP [fd79:35e2:9963:0000:f816:3eff:fe6d:7a2a]:1E61 rr\n" + " -> [fd79:35e2:9963:0000:f816:3eff:feca:b7bf]:08AE " + "Masq 3 0 0\n" + " -> [fd79:35e2:9963:0000:f816:3eff:fe9d:94df]:0D05 " + "Masq 2 0 0") + +CFG_FILE_TEMPLATE_v4 = ( + "# Configuration for Listener %(listener_id)s\n\n" + "net_namespace %(ns_name)s\n\n" + "virtual_server 10.0.0.37 7777 {\n" + " lb_algo rr\n" + " lb_kind NAT\n" + " protocol udp\n\n\n" + " # Configuration for Pool %(pool_id)s\n" + " # Configuration for Member %(member_id1)s\n" + " real_server 10.0.0.25 2222 {\n" + " weight 3\n" + " inhibit_on_failure\n" + " persistence_timeout 5\n" + " persistence_granularity 255.0.0.0\n\n" + " }\n\n" + " # Configuration for Member %(member_id2)s\n" + " real_server 10.0.0.35 3333 {\n" + " weight 2\n" + " inhibit_on_failure\n" + " persistence_timeout 5\n" + " persistence_granularity 255.0.0.0\n\n" + " }\n\n" + "}") + +CFG_FILE_TEMPLATE_v6 = ( + "# Configuration for Listener %(listener_id)s\n\n" + "net_namespace %(ns_name)s\n\n" + "virtual_server fd79:35e2:9963:0:f816:3eff:fe6d:7a2a 7777 {\n" + " lb_algo rr\n" + " lb_kind NAT\n" + " protocol udp\n\n\n" + " # Configuration for Pool %(pool_id)s\n" + " # Configuration for Member %(member_id1)s\n" + " real_server fd79:35e2:9963:0:f816:3eff:feca:b7bf 2222 {\n" + " weight 3\n" + " inhibit_on_failure\n" + " }\n\n" + " # Configuration for Member %(member_id2)s\n" + " real_server fd79:35e2:9963:0:f816:3eff:fe9d:94df 3333 {\n" + " weight 2\n" + " inhibit_on_failure\n" + " }\n\n" + "}") + +IPVSADM_OUTPUT_TEMPLATE = ( + "IP Virtual Server version 1.2.1 (size=4096)\n" + "Prot LocalAddress:Port Scheduler Flags\n" + " -> RemoteAddress:Port Forward Weight ActiveConn InActConn\n" + "UDP %(listener_ipport)s rr\n" + " -> %(member1_ipport)s Masq 3 0 0\n" + " -> %(member2_ipport)s Masq 2 0 0") + +IPVSADM_STATS_OUTPUT_TEMPLATE = ( + "IP Virtual Server version 1.2.1 (size=4096)\n" + "Prot LocalAddress:Port Conns InPkts OutPkts " + "InBytes OutBytes\n" + " -> RemoteAddress:Port\n" + "UDP %(listener_ipport)s 5 4264 5" + " 6387472 7490\n" + " -> %(member1_ipport)s 2 1706 2" + " 2555588 2996\n" + " -> %(member2_ipport)s 3 2558 3" + " 3831884 4494") + + +class LvsQueryTestCase(base.TestCase): + def setUp(self): + super(LvsQueryTestCase, self).setUp() + self.listener_id_v4 = uuidutils.generate_uuid() + self.pool_id_v4 = uuidutils.generate_uuid() + self.member_id1_v4 = uuidutils.generate_uuid() + self.member_id2_v4 = uuidutils.generate_uuid() + self.listener_id_v6 = uuidutils.generate_uuid() + self.pool_id_v6 = uuidutils.generate_uuid() + self.member_id1_v6 = uuidutils.generate_uuid() + self.member_id2_v6 = uuidutils.generate_uuid() + cfg_content_v4 = CFG_FILE_TEMPLATE_v4 % { + 'listener_id': self.listener_id_v4, + 'ns_name': constants.AMPHORA_NAMESPACE, + 'pool_id': self.pool_id_v4, + 'member_id1': self.member_id1_v4, + 'member_id2': self.member_id2_v4 + } + cfg_content_v6 = CFG_FILE_TEMPLATE_v6 % { + 'listener_id': self.listener_id_v6, + 'ns_name': constants.AMPHORA_NAMESPACE, + 'pool_id': self.pool_id_v6, + 'member_id1': self.member_id1_v6, + 'member_id2': self.member_id2_v6 + } + self.useFixture(test_utils.OpenFixture( + util.keepalived_lvs_cfg_path(self.listener_id_v4), cfg_content_v4)) + self.useFixture(test_utils.OpenFixture( + util.keepalived_lvs_cfg_path(self.listener_id_v6), cfg_content_v6)) + + @mock.patch('subprocess.check_output') + def test_get_listener_realserver_mapping(self, mock_check_output): + # Ipv4 resolver + input_listener_ip_port = '10.0.0.37:7777' + target_ns = constants.AMPHORA_NAMESPACE + mock_check_output.return_value = KERNAL_FILE_SAMPLE_V4 + result = lvs_query.get_listener_realserver_mapping( + target_ns, input_listener_ip_port) + expected = {'10.0.0.25:2222': {'status': 'UP', + 'Forward': 'Masq', + 'Weight': '3', + 'ActiveConn': '0', + 'InActConn': '0'}, + '10.0.0.35:3333': {'status': 'UP', + 'Forward': 'Masq', + 'Weight': '2', + 'ActiveConn': '0', + 'InActConn': '0'}} + self.assertEqual((True, expected), result) + + # Ipv6 resolver + input_listener_ip_port = '[fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7777' + mock_check_output.return_value = KERNAL_FILE_SAMPLE_V6 + result = lvs_query.get_listener_realserver_mapping( + target_ns, input_listener_ip_port) + expected = {'[fd79:35e2:9963:0:f816:3eff:feca:b7bf]:2222': + {'status': constants.UP, + 'Forward': 'Masq', + 'Weight': '3', + 'ActiveConn': '0', + 'InActConn': '0'}, + '[fd79:35e2:9963:0:f816:3eff:fe9d:94df]:3333': + {'status': constants.UP, + 'Forward': 'Masq', + 'Weight': '2', + 'ActiveConn': '0', + 'InActConn': '0'}} + self.assertEqual((True, expected), result) + + # negetive cases + mock_check_output.return_value = KERNAL_FILE_SAMPLE_V4 + for listener_ip_port in ['10.0.0.37:7776', '10.0.0.31:7777']: + result = lvs_query.get_listener_realserver_mapping( + target_ns, listener_ip_port) + self.assertEqual((False, {}), result) + + mock_check_output.return_value = KERNAL_FILE_SAMPLE_V6 + for listener_ip_port in [ + '[fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7776', + '[fd79:35e2:9973:0:f816:3eff:fe6d:7a2a]:7777']: + result = lvs_query.get_listener_realserver_mapping( + target_ns, listener_ip_port) + self.assertEqual((False, {}), result) + + def test_get_udp_listener_resource_ipports_nsname(self): + # ipv4 + res = lvs_query.get_udp_listener_resource_ipports_nsname( + self.listener_id_v4) + expected = {'Listener': {'id': self.listener_id_v4, + 'ipport': '10.0.0.37:7777'}, + 'Pool': {'id': self.pool_id_v4}, + 'Members': [{'id': self.member_id1_v4, + 'ipport': '10.0.0.25:2222'}, + {'id': self.member_id2_v4, + 'ipport': '10.0.0.35:3333'}]} + self.assertEqual((expected, constants.AMPHORA_NAMESPACE), res) + + # ipv6 + res = lvs_query.get_udp_listener_resource_ipports_nsname( + self.listener_id_v6) + expected = {'Listener': { + 'id': self.listener_id_v6, + 'ipport': '[fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7777'}, + 'Pool': {'id': self.pool_id_v6}, + 'Members': [ + {'id': self.member_id1_v6, + 'ipport': '[fd79:35e2:9963:0:f816:3eff:feca:b7bf]:2222'}, + {'id': self.member_id2_v6, + 'ipport': '[fd79:35e2:9963:0:f816:3eff:fe9d:94df]:3333'}]} + self.assertEqual((expected, constants.AMPHORA_NAMESPACE), res) + + @mock.patch('subprocess.check_output') + def test_get_udp_listener_pool_status(self, mock_check_output): + # test with ipv4 and ipv6 + mock_check_output.return_value = KERNAL_FILE_SAMPLE_V4 + res = lvs_query.get_udp_listener_pool_status(self.listener_id_v4) + expected = { + 'lvs': + {'uuid': self.pool_id_v4, + 'status': constants.UP, + 'members': {self.member_id1_v4: constants.UP, + self.member_id2_v4: constants.UP}}} + self.assertEqual(expected, res) + + mock_check_output.return_value = KERNAL_FILE_SAMPLE_V6 + res = lvs_query.get_udp_listener_pool_status(self.listener_id_v6) + expected = { + 'lvs': + {'uuid': self.pool_id_v6, + 'status': constants.UP, + 'members': {self.member_id1_v6: constants.UP, + self.member_id2_v6: constants.UP}}} + self.assertEqual(expected, res) + + @mock.patch('octavia.amphorae.backends.utils.keepalivedlvs_query.' + 'get_udp_listener_resource_ipports_nsname') + def test_get_udp_listener_pool_status_when_no_pool( + self, mock_get_resource_ipports): + # Just test with ipv4, ipv6 tests is same. + # the returned resource_ipport_mapping doesn't contains the 'Pool' + # resource, that means the listener doesn't have a pool resource, it + # isn't usable at this moment, then the pool status will + # return nothing. + mock_get_resource_ipports.return_value = ( + { + 'Listener': { + 'id': self.listener_id_v4, + 'ipport': '10.0.0.37:7777'}}, + constants.AMPHORA_NAMESPACE) + res = lvs_query.get_udp_listener_pool_status(self.listener_id_v4) + self.assertEqual({}, res) + + @mock.patch('octavia.amphorae.backends.utils.keepalivedlvs_query.' + 'get_udp_listener_resource_ipports_nsname') + def test_get_udp_listener_pool_status_when_no_members( + self, mock_get_resource_ipports): + # Just test with ipv4, ipv6 tests is same. + # the returned resource_ipport_mapping doesn't contains the 'Members' + # resources, that means the pool of listener doesn't have a enabled + # pool resource, so the pool is not usable, then the pool status will + # return DOWN. + mock_get_resource_ipports.return_value = ( + { + 'Listener': {'id': self.listener_id_v4, + 'ipport': '10.0.0.37:7777'}, + 'Pool': {'id': self.pool_id_v4}}, + constants.AMPHORA_NAMESPACE) + res = lvs_query.get_udp_listener_pool_status(self.listener_id_v4) + expected = {'lvs': { + 'uuid': self.pool_id_v4, + 'status': constants.DOWN, + 'members': {} + }} + self.assertEqual(expected, res) + + @mock.patch('octavia.amphorae.backends.utils.keepalivedlvs_query.' + 'get_listener_realserver_mapping') + def test_get_udp_listener_pool_status_when_not_get_realserver_result( + self, mock_get_mapping): + # This will hit if the kernel lvs file (/proc/net/ip_vs) + # lose its content. So at this moment, eventhough we configure the + # pool and member into udp keepalived config file, we have to set + # ths status of pool and its members to DOWN. + mock_get_mapping.return_value = (False, {}) + res = lvs_query.get_udp_listener_pool_status(self.listener_id_v4) + expected = { + 'lvs': + {'uuid': self.pool_id_v4, + 'status': constants.DOWN, + 'members': {self.member_id1_v4: constants.DOWN, + self.member_id2_v4: constants.DOWN}}} + self.assertEqual(expected, res) + + @mock.patch('subprocess.check_output') + def test_get_ipvsadm_info(self, mock_check_output): + for ip_list in [["10.0.0.37:7777", "10.0.0.25:2222", "10.0.0.35:3333"], + ["[fd79:35e2:9963:0:f816:3eff:fe6d:7a2a]:7777", + "[fd79:35e2:9963:0:f816:3eff:feca:b7bf]:2222", + "[fd79:35e2:9963:0:f816:3eff:fe9d:94df]:3333"]]: + mock_check_output.return_value = IPVSADM_OUTPUT_TEMPLATE % { + "listener_ipport": ip_list[0], + "member1_ipport": ip_list[1], + "member2_ipport": ip_list[2]} + res = lvs_query.get_ipvsadm_info(constants.AMPHORA_NAMESPACE) + # This expected result can referece on IPVSADM_OUTPUT_TEMPLATE, + # that means the function can get every element of the virtual + # server and the real servers. + expected = { + ip_list[0]: { + 'Listener': [('Prot', 'UDP'), + ('LocalAddress:Port', ip_list[0]), + ('Scheduler', 'rr')], + 'Members': [[('RemoteAddress:Port', ip_list[1]), + ('Forward', 'Masq'), ('Weight', '3'), + ('ActiveConn', '0'), ('InActConn', '0')], + [('RemoteAddress:Port', ip_list[2]), + ('Forward', 'Masq'), ('Weight', '2'), + ('ActiveConn', '0'), ('InActConn', '0')]]}} + self.assertEqual(expected, res) + + # ipvsadm stats + mock_check_output.return_value = IPVSADM_STATS_OUTPUT_TEMPLATE % { + "listener_ipport": ip_list[0], + "member1_ipport": ip_list[1], + "member2_ipport": ip_list[2]} + res = lvs_query.get_ipvsadm_info(constants.AMPHORA_NAMESPACE, + is_stats_cmd=True) + expected = { + ip_list[0]: + {'Listener': [('Prot', 'UDP'), + ('LocalAddress:Port', ip_list[0]), + ('Conns', '5'), + ('InPkts', '4264'), + ('OutPkts', '5'), + ('InBytes', '6387472'), + ('OutBytes', '7490')], + 'Members': [[('RemoteAddress:Port', ip_list[1]), + ('Conns', '2'), + ('InPkts', '1706'), + ('OutPkts', '2'), + ('InBytes', '2555588'), + ('OutBytes', '2996')], + [('RemoteAddress:Port', ip_list[2]), + ('Conns', '3'), + ('InPkts', '2558'), + ('OutPkts', '3'), + ('InBytes', '3831884'), + ('OutBytes', '4494')]]}} + self.assertEqual(expected, res) + + @mock.patch('subprocess.check_output') + @mock.patch("octavia.amphorae.backends.agent.api_server.util." + "is_udp_listener_running", return_value=True) + @mock.patch("octavia.amphorae.backends.agent.api_server.util." + "get_udp_listeners") + def test_get_udp_listeners_stats( + self, mock_get_listener, mock_is_running, mock_check_output): + # The ipv6 test is same with ipv4, so just test ipv4 here + mock_get_listener.return_value = [self.listener_id_v4] + output_list = list() + output_list.append(IPVSADM_OUTPUT_TEMPLATE % { + "listener_ipport": "10.0.0.37:7777", + "member1_ipport": "10.0.0.25:2222", + "member2_ipport": "10.0.0.35:3333"}) + output_list.append(IPVSADM_STATS_OUTPUT_TEMPLATE % { + "listener_ipport": "10.0.0.37:7777", + "member1_ipport": "10.0.0.25:2222", + "member2_ipport": "10.0.0.35:3333"}) + mock_check_output.side_effect = output_list + res = lvs_query.get_udp_listeners_stats() + # We can check the expected result referece the stats sample, + # that means this func can compute the stats info of single listener. + expected = {self.listener_id_v4: { + 'status': constants.OPEN, + 'stats': {'bin': 6387472, 'stot': 5, 'bout': 7490, + 'ereq': 0, 'scur': 0}}} + self.assertEqual(expected, res) + + # if no udp listener need to be collected. + # Then this function will return nothing. + mock_is_running.return_value = False + res = lvs_query.get_udp_listeners_stats() + self.assertIsNone(res) diff --git a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py index e5f2961e41..9d3e188f3d 100644 --- a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py +++ b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py @@ -41,6 +41,9 @@ FAKE_UUID_1 = uuidutils.generate_uuid() FAKE_VRRP_IP = '10.1.0.1' FAKE_MAC_ADDRESS = '123' FAKE_MTU = 1450 +FAKE_MEMBER_IP_PORT_NAME_1 = "10.0.0.10:1003" +FAKE_MEMBER_IP_PORT_NAME_2 = "10.0.0.11:1004" +FAKE_PROTOCOL = 'test-protocol' class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): @@ -61,9 +64,16 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.driver.cert_parser = mock.MagicMock() self.driver.client = mock.MagicMock() self.driver.jinja = mock.MagicMock() + self.driver.udp_jinja = mock.MagicMock() # Build sample Listener and VIP configs self.sl = sample_configs.sample_listener_tuple(tls=True, sni=True) + self.sl_udp = sample_configs.sample_listener_tuple( + proto=constants.PROTOCOL_UDP, + persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP, + persistence_timeout=33, + persistence_granularity='255.255.0.0', + monitor_proto=constants.HEALTH_MONITOR_UDP_CONNECT) self.amp = self.sl.load_balancer.amphorae[0] self.sv = sample_configs.sample_vip_tuple() self.lb = self.sl.load_balancer @@ -192,7 +202,21 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.amp, self.sl.id, 'fake_config') # start should be called once self.driver.client.reload_listener.assert_called_once_with( - self.amp, self.sl.id) + self.amp, self.sl.id, self.sl.protocol) + + def test_udp_update(self): + self.driver.udp_jinja.build_config.side_effect = ['fake_udp_config'] + + # Execute driver method + self.driver.update(self.sl_udp, self.sv) + + # upload only one config file + self.driver.client.upload_udp_config.assert_called_once_with( + self.amp, self.sl_udp.id, 'fake_udp_config') + + # start should be called once + self.driver.client.reload_listener.assert_called_once_with( + self.amp, self.sl_udp.id, self.sl_udp.protocol) def test_upload_cert_amp(self): self.driver.upload_cert_amp(self.amp, six.b('test')) @@ -200,10 +224,18 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.amp, six.b('test')) def test_stop(self): + self.driver.client.stop_listener.__name__ = 'stop_listener' # Execute driver method self.driver.stop(self.sl, self.sv) self.driver.client.stop_listener.assert_called_once_with( - self.amp, self.sl.id) + self.amp, self.sl.id, self.sl.protocol) + + def test_udp_stop(self): + self.driver.client.stop_listener.__name__ = 'stop_listener' + # Execute driver method - UDP case + self.driver.stop(self.sl_udp, self.sv) + self.driver.client.stop_listener.assert_called_once_with( + self.amp, self.sl_udp.id, self.sl_udp.protocol) def test_start(self): amp1 = mock.MagicMock() @@ -212,28 +244,46 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): listener = mock.MagicMock() listener.id = uuidutils.generate_uuid() listener.load_balancer.amphorae = [amp1, amp2] + listener.protocol = 'listener_protocol' + self.driver.client.start_listener.__name__ = 'start_listener' # Execute driver method self.driver.start(listener, self.sv) self.driver.client.start_listener.assert_called_once_with( - amp1, listener.id) + amp1, listener.id, 'listener_protocol') def test_start_with_amphora(self): # Execute driver method amp = mock.MagicMock() + self.driver.client.start_listener.__name__ = 'start_listener' self.driver.start(self.sl, self.sv, self.amp) self.driver.client.start_listener.assert_called_once_with( - self.amp, self.sl.id) + self.amp, self.sl.id, self.sl.protocol) self.driver.client.start_listener.reset_mock() amp.status = constants.DELETED self.driver.start(self.sl, self.sv, amp) self.driver.client.start_listener.assert_not_called() + def test_udp_start(self): + self.driver.client.start_listener.__name__ = 'start_listener' + # Execute driver method + self.driver.start(self.sl_udp, self.sv) + self.driver.client.start_listener.assert_called_once_with( + self.amp, self.sl_udp.id, self.sl_udp.protocol) + def test_delete(self): + self.driver.client.delete_listener.__name__ = 'delete_listener' # Execute driver method self.driver.delete(self.sl, self.sv) self.driver.client.delete_listener.assert_called_once_with( - self.amp, self.sl.id) + self.amp, self.sl.id, self.sl.protocol) + + def test_udp_delete(self): + self.driver.client.delete_listener.__name__ = 'delete_listener' + # Execute driver method + self.driver.delete(self.sl_udp, self.sv) + self.driver.client.delete_listener.assert_called_once_with( + self.amp, self.sl_udp.id, self.sl_udp.protocol) def test_get_info(self): self.driver.client.get_info.return_value = 'FAKE_INFO' @@ -482,9 +532,31 @@ class TestAmphoraAPIClientTest(base.TestCase): m.get("{base}/listeners/{listener_id}".format( base=self.base_url, listener_id=FAKE_UUID_1), json=listener) - status = self.driver.get_listener_status(self.amp, FAKE_UUID_1) + status = self.driver.get_listener_status(self.amp, FAKE_UUID_1, + protocol='TCP') self.assertEqual(listener, status) + @requests_mock.mock() + def test_get_udp_listener_status(self, m): + udp_listener = {"status": "ACTIVE", "type": "lvs", + "uuid": FAKE_UUID_1, + "pools": [{ + "UDP-Listener-%s-pool" % FAKE_UUID_1: + { + "status": "UP", + "members": [ + {FAKE_MEMBER_IP_PORT_NAME_1: "DOWN"}, + {FAKE_MEMBER_IP_PORT_NAME_2: "ACTIVE"}, + ] + } + }]} + m.get("{base}/listeners/{listener_id}".format( + base=self.base_url, listener_id=FAKE_UUID_1), + json=udp_listener) + status = self.driver.get_listener_status(self.amp, FAKE_UUID_1, + protocol='UDP') + self.assertEqual(udp_listener, status) + @requests_mock.mock() def test_get_listener_status_unauthorized(self, m): m.get("{base}/listeners/{listener_id}".format( @@ -526,7 +598,7 @@ class TestAmphoraAPIClientTest(base.TestCase): def test_start_listener(self, m): m.put("{base}/listeners/{listener_id}/start".format( base=self.base_url, listener_id=FAKE_UUID_1)) - self.driver.start_listener(self.amp, FAKE_UUID_1) + self.driver.start_listener(self.amp, FAKE_UUID_1, FAKE_PROTOCOL) self.assertTrue(m.called) @requests_mock.mock() @@ -536,7 +608,7 @@ class TestAmphoraAPIClientTest(base.TestCase): status_code=404, headers={'content-type': 'application/json'}) self.assertRaises(exc.NotFound, self.driver.start_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_start_listener_unauthorized(self, m): @@ -544,7 +616,7 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.start_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_start_listener_server_error(self, m): @@ -552,7 +624,7 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.start_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_start_listener_service_unavailable(self, m): @@ -560,13 +632,13 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.start_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_stop_listener(self, m): m.put("{base}/listeners/{listener_id}/stop".format( base=self.base_url, listener_id=FAKE_UUID_1)) - self.driver.stop_listener(self.amp, FAKE_UUID_1) + self.driver.stop_listener(self.amp, FAKE_UUID_1, FAKE_PROTOCOL) self.assertTrue(m.called) @requests_mock.mock() @@ -576,7 +648,7 @@ class TestAmphoraAPIClientTest(base.TestCase): status_code=404, headers={'content-type': 'application/json'}) self.assertRaises(exc.NotFound, self.driver.stop_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_stop_listener_unauthorized(self, m): @@ -584,7 +656,7 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.stop_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_stop_listener_server_error(self, m): @@ -592,7 +664,7 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.stop_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_stop_listener_service_unavailable(self, m): @@ -600,13 +672,13 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.stop_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_delete_listener(self, m): m.delete("{base}/listeners/{listener_id}".format( base=self.base_url, listener_id=FAKE_UUID_1), json={}) - self.driver.delete_listener(self.amp, FAKE_UUID_1) + self.driver.delete_listener(self.amp, FAKE_UUID_1, FAKE_PROTOCOL) self.assertTrue(m.called) @requests_mock.mock() @@ -615,7 +687,7 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=404, headers={'content-type': 'application/json'}) - self.driver.delete_listener(self.amp, FAKE_UUID_1) + self.driver.delete_listener(self.amp, FAKE_UUID_1, FAKE_PROTOCOL) self.assertTrue(m.called) @requests_mock.mock() @@ -624,7 +696,7 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=401) self.assertRaises(exc.Unauthorized, self.driver.delete_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_delete_listener_server_error(self, m): @@ -632,7 +704,7 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=500) self.assertRaises(exc.InternalServerError, self.driver.delete_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_delete_listener_service_unavailable(self, m): @@ -640,7 +712,7 @@ class TestAmphoraAPIClientTest(base.TestCase): base=self.base_url, listener_id=FAKE_UUID_1), status_code=503) self.assertRaises(exc.ServiceUnavailable, self.driver.delete_listener, - self.amp, FAKE_UUID_1) + self.amp, FAKE_UUID_1, FAKE_PROTOCOL) @requests_mock.mock() def test_upload_cert_pem(self, m): @@ -875,6 +947,68 @@ class TestAmphoraAPIClientTest(base.TestCase): self.assertRaises(exc.ServiceUnavailable, self.driver.upload_config, self.amp, FAKE_UUID_1, config) + @requests_mock.mock() + def test_upload_udp_config(self, m): + config = {"name": "fake_config"} + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url, + listener_id=FAKE_UUID_1), + json=config) + self.driver.upload_udp_config(self.amp, FAKE_UUID_1, config) + self.assertTrue(m.called) + + @requests_mock.mock() + def test_upload_udp_invalid_config(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url, + listener_id=FAKE_UUID_1), + status_code=400) + self.assertRaises(exc.InvalidRequest, self.driver.upload_udp_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_udp_config_unauthorized(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url, + listener_id=FAKE_UUID_1), + status_code=401) + self.assertRaises(exc.Unauthorized, self.driver.upload_udp_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_udp_config_server_error(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url, + listener_id=FAKE_UUID_1), + status_code=500) + self.assertRaises(exc.InternalServerError, + self.driver.upload_udp_config, + self.amp, FAKE_UUID_1, config) + + @requests_mock.mock() + def test_upload_udp_config_service_unavailable(self, m): + config = '{"name": "bad_config"}' + m.put( + "{base}/listeners/" + "{amphora_id}/{listener_id}/udp_listener".format( + amphora_id=self.amp.id, base=self.base_url, + listener_id=FAKE_UUID_1), + status_code=503) + self.assertRaises(exc.ServiceUnavailable, + self.driver.upload_udp_config, + self.amp, FAKE_UUID_1, config) + @requests_mock.mock() def test_plug_vip(self, m): m.post("{base}/plug/vip/{vip}".format( diff --git a/octavia/tests/unit/controller/healthmanager/health_drivers/test_update_db.py b/octavia/tests/unit/controller/healthmanager/health_drivers/test_update_db.py index ab18bcb520..8c76985a74 100644 --- a/octavia/tests/unit/controller/healthmanager/health_drivers/test_update_db.py +++ b/octavia/tests/unit/controller/healthmanager/health_drivers/test_update_db.py @@ -1079,6 +1079,57 @@ class TestUpdateHealthDb(base.TestCase): self.mock_session(), mock_lb.id, operating_status='ONLINE') + def test_update_health_forbid_to_stale_udp_listener_amphora(self): + health = { + "id": self.FAKE_UUID_1, + "listeners": {}, + "recv_time": time.time() + } + + mock_lb = mock.Mock() + mock_lb.id = self.FAKE_UUID_1 + mock_lb.pools = [] + mock_lb.listeners = [] + mock_lb.provisioning_status = constants.ACTIVE + mock_lb.operating_status = 'blah' + + # The default pool of udp listener1 has no enabled member + mock_member1 = mock.Mock() + mock_member1.id = 'member-id-1' + mock_member1.enabled = False + mock_pool1 = mock.Mock() + mock_pool1.id = "pool-id-1" + mock_pool1.members = [mock_member1] + mock_listener1 = mock.Mock() + mock_listener1.id = 'listener-id-1' + mock_listener1.default_pool = mock_pool1 + mock_listener1.protocol = constants.PROTOCOL_UDP + + # The default pool of udp listener2 has no member + mock_pool2 = mock.Mock() + mock_pool2.id = "pool-id-2" + mock_pool2.members = [] + mock_listener2 = mock.Mock() + mock_listener2.id = 'listener-id-2' + mock_listener2.default_pool = mock_pool2 + mock_listener2.protocol = constants.PROTOCOL_UDP + + # The udp listener3 has no default_pool + mock_listener3 = mock.Mock() + mock_listener3.id = 'listener-id-3' + mock_listener3.default_pool = None + mock_listener3.protocol = constants.PROTOCOL_UDP + + mock_lb.listeners.extend([mock_listener1, mock_listener2, + mock_listener3]) + mock_lb.pools.extend([mock_pool1, mock_pool2]) + + self.amphora_repo.get_lb_for_amphora.return_value = mock_lb + self.hm.update_health(health, '192.0.2.1') + self.assertTrue(self.amphora_repo.get_lb_for_amphora.called) + self.assertTrue(self.loadbalancer_repo.update.called) + self.assertTrue(self.amphora_health_repo.replace.called) + class TestUpdateStatsDb(base.TestCase): diff --git a/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py b/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py index 850e490e12..eca09dab3a 100644 --- a/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py +++ b/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py @@ -639,8 +639,12 @@ class TestAllowedAddressPairsDriver(base.TestCase): server=t_constants.MOCK_COMPUTE_ID, port_id=port2.get('id')) def test_update_vip(self): - listeners = [data_models.Listener(protocol_port=80, peer_port=1024), - data_models.Listener(protocol_port=443, peer_port=1025)] + listeners = [data_models.Listener(protocol_port=80, peer_port=1024, + protocol=constants.PROTOCOL_TCP), + data_models.Listener(protocol_port=443, peer_port=1025, + protocol=constants.PROTOCOL_TCP), + data_models.Listener(protocol_port=50, peer_port=1026, + protocol=constants.PROTOCOL_UDP)] vip = data_models.Vip(ip_address='10.0.0.2') lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip) list_sec_grps = self.driver.neutron_client.list_security_groups @@ -661,17 +665,27 @@ class TestAllowedAddressPairsDriver(base.TestCase): 'security_group_rule': { 'security_group_id': 'secgrp-1', 'direction': 'ingress', - 'protocol': 'TCP', + 'protocol': 'tcp', 'port_range_min': 1024, 'port_range_max': 1024, 'ethertype': 'IPv4' } } + expected_create_rule_udp_peer = { + 'security_group_rule': { + 'security_group_id': 'secgrp-1', + 'direction': 'ingress', + 'protocol': 'tcp', + 'port_range_min': 1026, + 'port_range_max': 1026, + 'ethertype': 'IPv4' + } + } expected_create_rule_2 = { 'security_group_rule': { 'security_group_id': 'secgrp-1', 'direction': 'ingress', - 'protocol': 'TCP', + 'protocol': 'tcp', 'port_range_min': 1025, 'port_range_max': 1025, 'ethertype': 'IPv4' @@ -681,20 +695,39 @@ class TestAllowedAddressPairsDriver(base.TestCase): 'security_group_rule': { 'security_group_id': 'secgrp-1', 'direction': 'ingress', - 'protocol': 'TCP', + 'protocol': 'tcp', 'port_range_min': 443, 'port_range_max': 443, 'ethertype': 'IPv4' } } + expected_create_rule_udp = { + 'security_group_rule': { + 'security_group_id': 'secgrp-1', + 'direction': 'ingress', + 'protocol': 'udp', + 'port_range_min': 50, + 'port_range_max': 50, + 'ethertype': 'IPv4' + } + } + create_rule.assert_has_calls([mock.call(expected_create_rule_1), + mock.call(expected_create_rule_udp_peer), mock.call(expected_create_rule_2), - mock.call(expected_create_rule_3)]) + mock.call(expected_create_rule_3), + mock.call(expected_create_rule_udp)], + any_order=True) def test_update_vip_when_listener_deleted(self): - listeners = [data_models.Listener(protocol_port=80), + listeners = [data_models.Listener(protocol_port=80, + protocol=constants.PROTOCOL_TCP), data_models.Listener( protocol_port=443, + protocol=constants.PROTOCOL_TCP, + provisioning_status=constants.PENDING_DELETE), + data_models.Listener( + protocol_port=50, protocol=constants.PROTOCOL_UDP, provisioning_status=constants.PENDING_DELETE)] vip = data_models.Vip(ip_address='10.0.0.2') lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip) @@ -703,7 +736,8 @@ class TestAllowedAddressPairsDriver(base.TestCase): fake_rules = { 'security_group_rules': [ {'id': 'rule-80', 'port_range_max': 80, 'protocol': 'tcp'}, - {'id': 'rule-22', 'port_range_max': 443, 'protocol': 'tcp'} + {'id': 'rule-22', 'port_range_max': 443, 'protocol': 'tcp'}, + {'id': 'rule-udp-50', 'port_range_max': 50, 'protocol': 'tcp'} ] } list_rules = self.driver.neutron_client.list_security_group_rules @@ -711,7 +745,8 @@ class TestAllowedAddressPairsDriver(base.TestCase): delete_rule = self.driver.neutron_client.delete_security_group_rule create_rule = self.driver.neutron_client.create_security_group_rule self.driver.update_vip(lb) - delete_rule.assert_called_once_with('rule-22') + delete_rule.assert_has_calls( + [mock.call('rule-22'), mock.call('rule-udp-50')]) self.assertTrue(create_rule.called) def test_update_vip_when_no_listeners(self): diff --git a/setup.cfg b/setup.cfg index 98e3965dc2..4b9ee73935 100644 --- a/setup.cfg +++ b/setup.cfg @@ -68,6 +68,8 @@ octavia.amphora.health_update_drivers = octavia.amphora.stats_update_drivers = stats_logger = octavia.controller.healthmanager.health_drivers.update_logging:StatsUpdateLogger stats_db = octavia.controller.healthmanager.health_drivers.update_db:UpdateStatsDb +octavia.amphora.udp_api_server = + keepalived_lvs = octavia.amphorae.backends.agent.api_server.keepalivedlvs:KeepalivedLvs octavia.controller.queues = noop_event_streamer = octavia.controller.queue.event_queue:EventStreamerNoop queue_event_streamer = octavia.controller.queue.event_queue:EventStreamerNeutron