From c00488143daf8e9306194ddcda636910bab532b6 Mon Sep 17 00:00:00 2001 From: Nir Magnezi Date: Thu, 12 Jan 2017 11:32:50 +0200 Subject: [PATCH] Fix the amphora image support for RH Linux flavors Not all Linux flavors accept the same type of configuration to manage NICs. The amphora-agent must be able to distinguish between different Linux flavors and choose the appropriate type of jinja2 NIC configuration template for each one, respectively. Up until now, The amphora-agent had no notion of the operating system it is running on, therefore it used NIC configuration templates that only match Debian based Linux flavors (mostly Ubuntu). Making it unusable for flavors such as RHEL, Fedora and CentOS. This fix enhances how the amphora-agent is handling NIC hot plugs. It will use the appropriate jinja2 template by checking the Amphora distribution name when needed. Co-Authored-By: Michael Johnson Closes-Bug #1548070 Change-Id: Id99948aec64656a0532afc68e146f0610bff1378 --- diskimage-create/diskimage-create.sh | 6 +- .../install.d/75-run_setup_install | 3 + .../configure.d/20-haproxy-selinux | 9 - .../post-install.d/20-setup-haproxy-log | 12 + .../configure.d/20-haproxy-selinux | 9 - .../post-install.d/20-disable-default-haproxy | 0 .../post-install.d/20-setup-haproxy-log | 12 + .../post-install.d/20-enable-keepalived | 6 - elements/no-resolvconf/README.rst | 2 + .../finalise.d/99-disable-resolv-conf | 4 + etc/octavia.conf | 8 +- .../backends/agent/api_server/amphora_info.py | 314 +-- .../backends/agent/api_server/listener.py | 7 +- .../backends/agent/api_server/osutils.py | 444 +++++ .../backends/agent/api_server/plug.py | 176 +- .../backends/agent/api_server/server.py | 11 +- .../templates/plug_vip_ethX.conf.j2 | 2 +- .../templates/rh_plug_port_ethX.conf.j2 | 47 + .../templates/rh_plug_vip_ethX.conf.j2 | 54 + .../templates/rh_plug_vip_ethX_alias.conf.j2 | 29 + .../templates/rh_route_ethX.conf.j2 | 20 + .../api_server/templates/systemd.conf.j2 | 6 +- .../api_server/templates/sysvinit.conf.j2 | 6 +- .../api_server/templates/upstart.conf.j2 | 7 +- .../backends/agent/api_server/util.py | 7 - .../templates/amphora_agent_conf.template | 2 + octavia/common/config.py | 2 - octavia/common/constants.py | 5 + octavia/common/exceptions.py | 4 + octavia/common/utils.py | 4 + octavia/tests/common/utils.py | 3 + .../backend/agent/api_server/test_server.py | 1708 +++++++++++++---- .../agent/api_server/test_amphora_info.py | 206 ++ .../backends/agent/api_server/test_osutils.py | 217 +++ .../backends/agent/api_server/test_plug.py | 25 +- .../backends/agent/api_server/test_utils.py | 47 - octavia/tests/unit/common/test_utils.py | 6 + ...rt-for-amphora-agent-cd3e9f9f519b9ff2.yaml | 18 + 38 files changed, 2620 insertions(+), 828 deletions(-) delete mode 100755 elements/haproxy-octavia-ubuntu/os-refresh-config/configure.d/20-haproxy-selinux create mode 100755 elements/haproxy-octavia-ubuntu/post-install.d/20-setup-haproxy-log delete mode 100755 elements/haproxy-octavia/os-refresh-config/configure.d/20-haproxy-selinux mode change 100644 => 100755 elements/haproxy-octavia/post-install.d/20-disable-default-haproxy create mode 100755 elements/haproxy-octavia/post-install.d/20-setup-haproxy-log delete mode 100755 elements/keepalived-octavia/post-install.d/20-enable-keepalived create mode 100644 octavia/amphorae/backends/agent/api_server/osutils.py create mode 100644 octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_ethX.conf.j2 create mode 100644 octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX.conf.j2 create mode 100644 octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX_alias.conf.j2 create mode 100644 octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 create mode 100644 octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py create mode 100644 octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py delete mode 100644 octavia/tests/unit/amphorae/backends/agent/api_server/test_utils.py create mode 100644 releasenotes/notes/add-rh-flavors-support-for-amphora-agent-cd3e9f9f519b9ff2.yaml diff --git a/diskimage-create/diskimage-create.sh b/diskimage-create/diskimage-create.sh index 6437b91b4c..8211e1b8bf 100755 --- a/diskimage-create/diskimage-create.sh +++ b/diskimage-create/diskimage-create.sh @@ -333,21 +333,21 @@ if [ "$AMP_BASEOS" = "ubuntu" ]; then export UBUNTU_MIRROR="$BASE_OS_MIRROR" fi elif [ "$AMP_BASEOS" = "fedora" ]; then - AMP_element_sequence=${AMP_element_sequence:-"base vm fedora"} + AMP_element_sequence=${AMP_element_sequence:-"base vm fedora selinux-permissive"} AMP_element_sequence="$AMP_element_sequence $AMP_BACKEND" if [ "$BASE_OS_MIRROR" ]; then AMP_element_sequence="$AMP_element_sequence fedora-mirror" export FEDORA_MIRROR="$BASE_OS_MIRROR" fi elif [ "$AMP_BASEOS" = "centos" ]; then - AMP_element_sequence=${AMP_element_sequence:-"base vm centos7 epel"} + AMP_element_sequence=${AMP_element_sequence:-"base vm centos7 epel selinux-permissive"} AMP_element_sequence="$AMP_element_sequence $AMP_BACKEND" if [ "$BASE_OS_MIRROR" ]; then AMP_element_sequence="$AMP_element_sequence centos-mirror" export CENTOS_MIRROR="$BASE_OS_MIRROR" fi elif [ "$AMP_BASEOS" = "rhel" ]; then - AMP_element_sequence=${AMP_element_sequence:-"base vm rhel7"} + AMP_element_sequence=${AMP_element_sequence:-"base vm rhel7 selinux-permissive"} AMP_element_sequence="$AMP_element_sequence $AMP_BACKEND" fi diff --git a/elements/amphora-agent/install.d/75-run_setup_install b/elements/amphora-agent/install.d/75-run_setup_install index b4c4695a77..3ae520ec37 100755 --- a/elements/amphora-agent/install.d/75-run_setup_install +++ b/elements/amphora-agent/install.d/75-run_setup_install @@ -8,6 +8,9 @@ set -o pipefail pip install -U -c /opt/upper-constraints.txt /opt/amphora-agent +# Accommodate centos default install location +ln -s /bin/amphora-agent /usr/local/bin/amphora-agent || true + mkdir /etc/octavia # we assume certs, etc will come in through the config drive mkdir /etc/octavia/certs diff --git a/elements/haproxy-octavia-ubuntu/os-refresh-config/configure.d/20-haproxy-selinux b/elements/haproxy-octavia-ubuntu/os-refresh-config/configure.d/20-haproxy-selinux deleted file mode 100755 index f5d7acfd9e..0000000000 --- a/elements/haproxy-octavia-ubuntu/os-refresh-config/configure.d/20-haproxy-selinux +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -eux -set -o pipefail - -# Allow haproxy to proxy any port if SELinux is in enforcing mode -# https://bugs.launchpad.net/tripleo/+bug/1339938 -if [[ -x /usr/sbin/semanage ]]; then - setsebool -P haproxy_connect_any 1 -fi diff --git a/elements/haproxy-octavia-ubuntu/post-install.d/20-setup-haproxy-log b/elements/haproxy-octavia-ubuntu/post-install.d/20-setup-haproxy-log new file mode 100755 index 0000000000..0a69f0d5da --- /dev/null +++ b/elements/haproxy-octavia-ubuntu/post-install.d/20-setup-haproxy-log @@ -0,0 +1,12 @@ +#!/bin/bash + +set -eu +set -o pipefail + +if [[ -d /etc/rsyslog.d ]] && [[ ! -e /etc/rsyslog.d/49-haproxy.conf ]]; then + cat >> /etc/rsyslog.d/49-haproxy.conf <> /etc/rsyslog.d/49-haproxy.conf < /etc/dhclient-enter-hooks chmod +x /etc/dhclient-enter-hooks fi + +if [ -e /etc/nsswitch.conf ]; then + sed -i -e "/hosts:/ s/dns//g" /etc/nsswitch.conf +fi diff --git a/etc/octavia.conf b/etc/octavia.conf index ffa026f6fa..0406e9ca71 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -224,7 +224,13 @@ [amphora_agent] # agent_server_ca = /etc/octavia/certs/client_ca.pem # agent_server_cert = /etc/octavia/certs/server.pem -# agent_server_network_dir = /etc/netns/amphora-haproxy/network/interfaces.d/ + +# Defaults for agent_server_network_dir when not specified here are: +# Ubuntu: /etc/netns/amphora-haproxy/network/interfaces.d/ +# Centos/fedora/rhel: /etc/netns/amphora-haproxy/sysconfig/network-scripts/ +# +# agent_server_network_dir = + # agent_server_network_file = # agent_request_read_timeout = 120 diff --git a/octavia/amphorae/backends/agent/api_server/amphora_info.py b/octavia/amphorae/backends/agent/api_server/amphora_info.py index e09cedd0c1..b526995688 100644 --- a/octavia/amphorae/backends/agent/api_server/amphora_info.py +++ b/octavia/amphorae/backends/agent/api_server/amphora_info.py @@ -31,177 +31,177 @@ from octavia.common import constants as consts LOG = logging.getLogger(__name__) -def compile_amphora_info(): - return flask.jsonify( - {'hostname': socket.gethostname(), - 'haproxy_version': _get_version_of_installed_package('haproxy'), - 'api_version': api_server.VERSION}) +class AmphoraInfo(object): + def __init__(self, osutils): + self._osutils = osutils + def compile_amphora_info(self): + return flask.jsonify( + {'hostname': socket.gethostname(), + 'haproxy_version': + self._get_version_of_installed_package('haproxy'), + 'api_version': api_server.VERSION}) -def compile_amphora_details(): - listener_list = util.get_listeners() - meminfo = _get_meminfo() - cpu = _cpu() - st = os.statvfs('/') - return flask.jsonify( - {'hostname': socket.gethostname(), - 'haproxy_version': _get_version_of_installed_package('haproxy'), - 'api_version': api_server.VERSION, - 'networks': _get_networks(), - 'active': True, - 'haproxy_count': _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': [ - _load()], - 'topology': consts.TOPOLOGY_SINGLE, - 'topology_status': consts.TOPOLOGY_STATUS_OK, - 'listeners': listener_list, - 'packages': {}}) + def compile_amphora_details(self): + listener_list = util.get_listeners() + meminfo = self._get_meminfo() + cpu = self._cpu() + st = os.statvfs('/') + return flask.jsonify( + {'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': {}}) + def _get_version_of_installed_package(self, name): -def _get_version_of_installed_package(name): - cmd = "dpkg --status {name}".format(name=name) - out = subprocess.check_output(cmd.split()) - m = re.search(b'Version: .*', out) - return m.group(0)[len('Version: '):] + cmd = self._osutils.cmd_get_version_of_installed_package(name) + out = subprocess.check_output(cmd.split()) + m = re.search(b'Version: .*', out) + return m.group(0)[len('Version: '):] + def _count_haproxy_processes(self, listener_list): + num = 0 + for listener_id in listener_list: + if util.is_listener_running(listener_id): + # optional check if it's still running + num += 1 + return num -def _count_haproxy_processes(listener_list): - num = 0 - for listener_id in listener_list: - if util.is_listener_running(listener_id): - # optional check if it's still running - num += 1 - return num + def _get_meminfo(self): + re_parser = re.compile(r'^(?P\S*):\s*(?P\d*)\s*kB') + result = dict() + with open('/proc/meminfo', 'r') as meminfo: + for line in meminfo: + match = re_parser.match(line) + if not match: + continue # skip lines that don't parse + key, value = match.groups(['key', 'value']) + result[key] = int(value) + return result + def _cpu(self): + with open('/proc/stat') as f: + cpu = f.readline() + vals = cpu.split(' ') + return { + 'user': vals[2], + 'nice': vals[3], + 'system': vals[4], + 'idle': vals[5], + 'iowait': vals[6], + 'irq': vals[7], + 'softirq': vals[8], + 'total': sum([int(i) for i in vals[2:]]) + } -def _get_meminfo(): - re_parser = re.compile(r'^(?P\S*):\s*(?P\d*)\s*kB') - result = dict() - for line in open('/proc/meminfo'): - match = re_parser.match(line) - if not match: - continue # skip lines that don't parse - key, value = match.groups(['key', 'value']) - result[key] = int(value) - return result + def _load(self): + with open('/proc/loadavg') as f: + load = f.readline() + vals = load.split(' ') + return vals[:3] + def _get_networks(self): + networks = dict() + with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns: + for interface in netns.get_links(): + interface_name = None + for item in interface['attrs']: + if (item[0] == 'IFLA_IFNAME' + and not item[1].startswith('eth')): + break + elif item[0] == 'IFLA_IFNAME': + interface_name = item[1] + if item[0] == 'IFLA_STATS64': + networks[interface_name] = dict( + network_tx=item[1]['tx_bytes'], + network_rx=item[1]['rx_bytes']) + return networks -def _cpu(): - with open('/proc/stat') as f: - cpu = f.readline() - vals = cpu.split(' ') - return { - 'user': vals[2], - 'nice': vals[3], - 'system': vals[4], - 'idle': vals[5], - 'iowait': vals[6], - 'irq': vals[7], - 'softirq': vals[8], - 'total': sum([int(i) for i in vals[2:]]) - } + def get_interface(self, ip_addr): + try: + ip_version = ipaddress.ip_address(six.text_type(ip_addr)).version + except Exception: + return flask.make_response( + flask.jsonify(dict(message="Invalid IP address")), 400) -def _load(): - with open('/proc/loadavg') as f: - load = f.readline() - vals = load.split(' ') - return vals[:3] + if ip_version == 4: + address_format = netifaces.AF_INET + elif ip_version == 6: + address_format = netifaces.AF_INET6 + else: + return flask.make_response( + flask.jsonify(dict(message="Bad IP address version")), 400) + # We need to normalize the address as IPv6 has multiple representations + # fe80:0000:0000:0000:f816:3eff:fef2:2058 == fe80::f816:3eff:fef2:2058 + normalized_addr = socket.inet_ntop(address_format, + socket.inet_pton(address_format, + ip_addr)) -def _get_networks(): - networks = dict() - with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns: - for interface in netns.get_links(): - interface_name = None - for item in interface['attrs']: - if item[0] == 'IFLA_IFNAME' and not item[1].startswith('eth'): - break - elif item[0] == 'IFLA_IFNAME': - interface_name = item[1] - if item[0] == 'IFLA_STATS64': - networks[interface_name] = dict( - network_tx=item[1]['tx_bytes'], - network_rx=item[1]['rx_bytes']) - return networks + with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns: + for addr in netns.get_addr(): + # Save the interface index as IPv6 records don't list a + # textual interface + interface_idx = addr['index'] + # Save the address family (IPv4/IPv6) for use normalizing + # the IP address for comparison + interface_af = addr['family'] + # Search through the attributes of each address record + for attr in addr['attrs']: + # Look for the attribute name/value pair for the address + if attr[0] == 'IFA_ADDRESS': + # Compare the normalized address with the address we + # we are looking for. Since we have matched the name + # above, attr[1] is the address value + if normalized_addr == socket.inet_ntop( + interface_af, + socket.inet_pton(interface_af, attr[1])): + # Lookup the matching interface name by + # getting the interface with the index we found + # in the above address search + lookup_int = netns.get_links(interface_idx) + # Search through the attributes of the matching + # interface record + for int_attr in lookup_int[0]['attrs']: + # Look for the attribute name/value pair + # that includes the interface name + if int_attr[0] == 'IFLA_IFNAME': + # Return the response with the matching + # interface name that is in int_attr[1] + # for the matching interface attribute + # name + return flask.make_response( + flask.jsonify( + dict(message='OK', + interface=int_attr[1])), 200) -def get_interface(ip_addr): - - try: - ip_version = ipaddress.ip_address(six.text_type(ip_addr)).version - except Exception: return flask.make_response( - flask.jsonify(dict(message="Invalid IP address")), 400) - - if ip_version == 4: - address_format = netifaces.AF_INET - elif ip_version == 6: - address_format = netifaces.AF_INET6 - else: - return flask.make_response( - flask.jsonify(dict(message="Bad IP address version")), 400) - - # We need to normalize the address as IPv6 has multiple representations - # fe80:0000:0000:0000:f816:3eff:fef2:2058 == fe80::f816:3eff:fef2:2058 - normalized_addr = socket.inet_ntop(address_format, - socket.inet_pton(address_format, - ip_addr)) - - with pyroute2.NetNS(consts.AMPHORA_NAMESPACE) as netns: - for addr in netns.get_addr(): - # Save the interface index as IPv6 records don't list a - # textual interface - interface_idx = addr['index'] - # Save the address family (IPv4/IPv6) for use normalizing - # the IP address for comparison - interface_af = addr['family'] - # Search through the attributes of each address record - for attr in addr['attrs']: - # Look for the attribute name/value pair for the address - if attr[0] == 'IFA_ADDRESS': - # Compare the normalized address with the address we - # we are looking for. Since we have matched the name - # above, attr[1] is the address value - if normalized_addr == socket.inet_ntop( - interface_af, - socket.inet_pton(interface_af, attr[1])): - - # Lookup the matching interface name by - # getting the interface with the index we found - # in the above address search - lookup_int = netns.get_links(interface_idx) - # Search through the attributes of the matching - # interface record - for int_attr in lookup_int[0]['attrs']: - # Look for the attribute name/value pair - # that includes the interface name - if int_attr[0] == 'IFLA_IFNAME': - # Return the response with the matching - # interface name that is in int_attr[1] - # for the matching interface attribute - # name - return flask.make_response( - flask.jsonify( - dict(message='OK', - interface=int_attr[1])), 200) - - return flask.make_response( - flask.jsonify(dict(message="Error interface not found " - "for IP address")), 404) + flask.jsonify(dict(message="Error interface not found " + "for IP address")), 404) diff --git a/octavia/amphorae/backends/agent/api_server/listener.py b/octavia/amphorae/backends/agent/api_server/listener.py index b853cd5b05..99d44e092f 100644 --- a/octavia/amphorae/backends/agent/api_server/listener.py +++ b/octavia/amphorae/backends/agent/api_server/listener.py @@ -26,6 +26,7 @@ import jinja2 import six from werkzeug import exceptions +from octavia.amphorae.backends.agent.api_server import osutils from octavia.amphorae.backends.agent.api_server import util from octavia.amphorae.backends.utils import haproxy_query as query from octavia.common import constants as consts @@ -74,6 +75,9 @@ class Wrapped(object): class Listener(object): + def __init__(self): + self._osutils = osutils.BaseOS.get_os_util() + def get_haproxy_config(self, listener_id): """Gets the haproxy config @@ -171,7 +175,8 @@ class Listener(object): respawn_count=util.CONF.haproxy_amphora.respawn_count, respawn_interval=(util.CONF.haproxy_amphora. respawn_interval), - amphora_nsname=consts.AMPHORA_NAMESPACE + amphora_nsname=consts.AMPHORA_NAMESPACE, + HasIFUPAll=self._osutils.has_ifup_all() ) text_file.write(text) diff --git a/octavia/amphorae/backends/agent/api_server/osutils.py b/octavia/amphorae/backends/agent/api_server/osutils.py new file mode 100644 index 0000000000..172202c94c --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/osutils.py @@ -0,0 +1,444 @@ +# Copyright 2017 Red Hat, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os +import platform +import shutil +import stat +import subprocess + +import flask +import ipaddress +import jinja2 +from oslo_config import cfg +import six +from werkzeug import exceptions + +from octavia.common import constants as consts +from octavia.common import exceptions as octavia_exceptions +from octavia.common import utils +from octavia.i18n import _LE + +CONF = cfg.CONF + +LOG = logging.getLogger(__name__) + +j2_env = jinja2.Environment(autoescape=True, loader=jinja2.FileSystemLoader( + os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES)) + + +class BaseOS(object): + + def __init__(self, os_name): + self.os_name = os_name + + @classmethod + def get_os_util(cls): + os_name = platform.linux_distribution(full_distribution_name=False)[0] + for subclass in BaseOS.__subclasses__(): + if subclass.is_os_name(os_name): + return subclass(os_name) + raise octavia_exceptions.InvalidAmphoraOperatingSystem(os_name=os_name) + + def get_network_interface_file(self, interface): + if CONF.amphora_agent.agent_server_network_file: + return CONF.amphora_agent.agent_server_network_file + if CONF.amphora_agent.agent_server_network_dir: + return os.path.join(CONF.amphora_agent.agent_server_network_dir, + interface) + network_dir = consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format( + netns=consts.AMPHORA_NAMESPACE) + return os.path.join(network_dir, interface) + + def create_netns_dir(self, network_dir, netns_network_dir, ignore=None): + # We need to setup the netns network directory so that the ifup + # commands used here and in the startup scripts "sees" the right + # interfaces and scripts. + os.makedirs('/etc/netns/' + consts.AMPHORA_NAMESPACE) + shutil.copytree( + network_dir, + '/etc/netns/{netns}/{net_dir}'.format( + netns=consts.AMPHORA_NAMESPACE, + net_dir=netns_network_dir), + symlinks=True, + ignore=ignore) + + def write_vip_interface_file(self, interface_file_path, + primary_interface, vip, ip, broadcast, + netmask, gateway, mtu, vrrp_ip, vrrp_version, + render_host_routes, template_vip): + # write interface file + + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + + # If we are using a consolidated interfaces file, just append + # otherwise clear the per interface file as we are rewriting it + # TODO(johnsom): We need a way to clean out old interfaces records + if CONF.amphora_agent.agent_server_network_file: + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + else: + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + + with os.fdopen(os.open(interface_file_path, flags, mode), + 'w') as text_file: + text = template_vip.render( + interface=primary_interface, + vip=vip, + vip_ipv6=ip.version is 6, + prefix=utils.netmask_to_prefix(netmask), + broadcast=broadcast, + netmask=netmask, + gateway=gateway, + mtu=mtu, + vrrp_ip=vrrp_ip, + vrrp_ipv6=vrrp_version is 6, + host_routes=render_host_routes, + ) + text_file.write(text) + + def write_port_interface_file(self, netns_interface, fixed_ips, mtu, + interface_file_path, template_port): + # write interface file + + # If we are using a consolidated interfaces file, just append + # otherwise clear the per interface file as we are rewriting it + # TODO(johnsom): We need a way to clean out old interfaces records + if CONF.amphora_agent.agent_server_network_file: + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + else: + 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(interface_file_path, flags, mode), + 'w') as text_file: + text = self._generate_network_file_text(netns_interface, fixed_ips, + mtu, template_port) + text_file.write(text) + + def _generate_network_file_text(self, netns_interface, fixed_ips, mtu, + template_port): + text = '' + if fixed_ips is None: + text = template_port.render(interface=netns_interface) + else: + for index, fixed_ip in enumerate(fixed_ips, -1): + if index == -1: + netns_ip_interface = netns_interface + else: + netns_ip_interface = "{int}:{ip}".format( + int=netns_interface, ip=index) + try: + ip_addr = fixed_ip['ip_address'] + cidr = fixed_ip['subnet_cidr'] + ip = ipaddress.ip_address( + ip_addr if six.text_type == type( + ip_addr) else six.u(ip_addr)) + network = ipaddress.ip_network( + cidr if six.text_type == type( + cidr) else six.u(cidr)) + broadcast = network.broadcast_address.exploded + netmask = (network.prefixlen if ip.version is 6 + else network.netmask.exploded) + host_routes = self.get_host_routes(fixed_ip) + + except ValueError: + return flask.make_response(flask.jsonify(dict( + message="Invalid network IP")), 400) + new_text = template_port.render(interface=netns_ip_interface, + ipv6=ip.version is 6, + ip_address=ip.exploded, + broadcast=broadcast, + netmask=netmask, + mtu=mtu, + host_routes=host_routes) + text = '\n'.join([text, new_text]) + return text + + def get_host_routes(self, fixed_ip): + host_routes = [] + for hr in fixed_ip.get('host_routes', []): + network = ipaddress.ip_network( + hr['destination'] if isinstance( + hr['destination'], six.text_type) else + six.u(hr['destination'])) + host_routes.append({'network': network, 'gw': hr['nexthop']}) + return host_routes + + def _bring_if_up(self, interface, what): + # Note, we are not using pyroute2 for this as it is not /etc/netns + # aware. + cmd = ("ip netns exec {ns} ifup {params}".format( + ns=consts.AMPHORA_NAMESPACE, params=interface)) + try: + subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + LOG.error(_LE('Failed to if up {0} due to ' + 'error: {1}').format(interface, str(e))) + raise exceptions.HTTPException( + response=flask.make_response(flask.jsonify(dict( + message='Error plugging {0}'.format(what), + details=e.output)), 500)) + + def _bring_if_down(self, interface): + # Note, we are not using pyroute2 for this as it is not /etc/netns + # aware. + cmd = ("ip netns exec {ns} ifdown {params}".format( + ns=consts.AMPHORA_NAMESPACE, params=interface)) + try: + subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) + except subprocess.CalledProcessError: + pass + + def bring_interfaces_up(self, ip, primary_interface, secondary_interface): + self._bring_if_down(primary_interface) + self._bring_if_down(secondary_interface) + self._bring_if_up(primary_interface, 'VIP') + self._bring_if_up(secondary_interface, 'VIP') + + def has_ifup_all(self): + return True + + +class Ubuntu(BaseOS): + + ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2' + ETH_X_VIP_CONF = 'plug_vip_ethX.conf.j2' + + @classmethod + def is_os_name(cls, os_name): + return os_name in ['Ubuntu'] + + def cmd_get_version_of_installed_package(self, package_name): + return "dpkg --status {name}".format(name=package_name) + + def get_network_interface_file(self, interface): + if CONF.amphora_agent.agent_server_network_file: + return CONF.amphora_agent.agent_server_network_file + if CONF.amphora_agent.agent_server_network_dir: + return os.path.join(CONF.amphora_agent.agent_server_network_dir, + interface + '.cfg') + network_dir = consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format( + netns=consts.AMPHORA_NAMESPACE) + return os.path.join(network_dir, interface + '.cfg') + + def get_network_path(self): + return '/etc/network' + + def get_netns_network_dir(self): + network_dir = self.get_network_path() + return os.path.basename(network_dir) + + def create_netns_dir( + self, network_dir=None, netns_network_dir=None, ignore=None): + if not netns_network_dir: + netns_network_dir = self.get_netns_network_dir() + if not network_dir: + network_dir = self.get_network_path() + if not ignore: + ignore = shutil.ignore_patterns('eth0*', 'openssh*') + super(Ubuntu, self).create_netns_dir( + network_dir, netns_network_dir, ignore) + + def write_interfaces_file(self): + name = '/etc/netns/{}/network/interfaces'.format( + consts.AMPHORA_NAMESPACE) + 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(name, flags, mode), 'w') as int_file: + int_file.write('auto lo\n') + int_file.write('iface lo inet loopback\n') + if not CONF.amphora_agent.agent_server_network_file: + int_file.write('source /etc/netns/{}/network/' + 'interfaces.d/*.cfg\n'.format( + consts.AMPHORA_NAMESPACE)) + + def write_vip_interface_file(self, interface_file_path, + primary_interface, vip, ip, broadcast, + netmask, gateway, mtu, vrrp_ip, vrrp_version, + render_host_routes, template_vip=None): + if not template_vip: + template_vip = j2_env.get_template(self.ETH_X_VIP_CONF) + super(Ubuntu, self).write_vip_interface_file( + interface_file_path, primary_interface, vip, ip, broadcast, + netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes, + template_vip) + + def write_port_interface_file(self, netns_interface, fixed_ips, mtu, + interface_file_path=None, + template_port=None): + if not interface_file_path: + interface_file_path = self.get_network_interface_file( + netns_interface) + if not template_port: + template_port = j2_env.get_template(self.ETH_X_PORT_CONF) + super(Ubuntu, self).write_port_interface_file( + netns_interface, fixed_ips, mtu, interface_file_path, + template_port) + + def has_ifup_all(self): + return True + + +class RH(BaseOS): + + ETH_X_PORT_CONF = 'rh_plug_port_ethX.conf.j2' + ETH_X_VIP_CONF = 'rh_plug_vip_ethX.conf.j2' + ETH_X_ALIAS_VIP_CONF = 'rh_plug_vip_ethX_alias.conf.j2' + ROUTE_ETH_X_CONF = 'rh_route_ethX.conf.j2' + + @classmethod + def is_os_name(cls, os_name): + return os_name in ['fedora', 'redhat', 'centos'] + + def cmd_get_version_of_installed_package(self, package_name): + return "rpm -qi {name}".format(name=package_name) + + def get_network_interface_file(self, interface): + if CONF.amphora_agent.agent_server_network_file: + return CONF.amphora_agent.agent_server_network_file + if CONF.amphora_agent.agent_server_network_dir: + return os.path.join(CONF.amphora_agent.agent_server_network_dir, + 'ifcfg-' + interface) + network_dir = consts.RH_AMP_NET_DIR_TEMPLATE.format( + netns=consts.AMPHORA_NAMESPACE) + return os.path.join(network_dir, 'ifcfg-' + interface) + + def get_alias_network_interface_file(self, interface): + return self.get_network_interface_file(interface + ':0') + + def get_static_routes_interface_file(self, interface): + return self.get_network_interface_file('route-' + interface) + + def get_network_path(self): + return '/etc/sysconfig/network-scripts' + + def get_netns_network_dir(self): + network_full_path = self.get_network_path() + network_basename = os.path.basename(network_full_path) + network_dirname = os.path.dirname(network_full_path) + network_prefixdir = os.path.basename(network_dirname) + return os.path.join(network_prefixdir, network_basename) + + def create_netns_dir( + self, network_dir=None, netns_network_dir=None, ignore=None): + if not netns_network_dir: + netns_network_dir = self.get_netns_network_dir() + if not network_dir: + network_dir = self.get_network_path() + if not ignore: + ignore = shutil.ignore_patterns('ifcfg-eth0*', 'ifcfg-lo*') + super(RH, self).create_netns_dir( + network_dir, netns_network_dir, ignore) + + # Copy /etc/sysconfig/network file + src = '/etc/sysconfig/network' + dst = '/etc/netns/{netns}/sysconfig'.format( + netns=consts.AMPHORA_NAMESPACE) + shutil.copy2(src, dst) + + def write_interfaces_file(self): + # No interfaces file in RH based flavors + return + + def write_vip_interface_file(self, interface_file_path, + primary_interface, vip, ip, broadcast, + netmask, gateway, mtu, vrrp_ip, vrrp_version, + render_host_routes, template_vip=None): + if not template_vip: + template_vip = j2_env.get_template(self.ETH_X_VIP_CONF) + super(RH, self).write_vip_interface_file( + interface_file_path, primary_interface, vip, ip, broadcast, + netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes, + template_vip) + + if ip.version == 4: + # Create an IPv4 alias interface, needed in RH based flavors + alias_interface_file_path = self.get_alias_network_interface_file( + primary_interface) + template_vip_alias = j2_env.get_template(self.ETH_X_ALIAS_VIP_CONF) + super(RH, self).write_vip_interface_file( + alias_interface_file_path, primary_interface, vip, ip, + broadcast, netmask, gateway, mtu, vrrp_ip, vrrp_version, + render_host_routes, template_vip_alias) + + if render_host_routes: + routes_interface_file_path = ( + self.get_static_routes_interface_file(primary_interface)) + template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF) + + self.write_static_routes_interface_file( + routes_interface_file_path, primary_interface, + render_host_routes, template_routes) + + def write_static_routes_interface_file(self, interface_file_path, + interface, host_routes, + template_routes): + # write static routes interface file + + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + + # TODO(johnsom): We need a way to clean out old interfaces records + if CONF.amphora_agent.agent_server_network_file: + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + else: + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + + with os.fdopen(os.open(interface_file_path, flags, mode), + 'w') as text_file: + text = template_routes.render( + interface=interface, + host_routes=host_routes, + ) + text_file.write(text) + + def write_port_interface_file(self, netns_interface, fixed_ips, mtu, + interface_file_path=None, + template_port=None): + if not interface_file_path: + interface_file_path = self.get_network_interface_file( + netns_interface) + if not template_port: + template_port = j2_env.get_template(self.ETH_X_PORT_CONF) + super(RH, self).write_port_interface_file( + netns_interface, fixed_ips, mtu, interface_file_path, + template_port) + + if fixed_ips: + host_routes = [] + for fixed_ip in fixed_ips: + host_routes.extend(self.get_host_routes(fixed_ip)) + + routes_interface_file_path = ( + self.get_static_routes_interface_file(netns_interface)) + template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF) + + self.write_static_routes_interface_file( + routes_interface_file_path, netns_interface, + host_routes, template_routes) + + def bring_interfaces_up(self, ip, primary_interface, secondary_interface): + if ip.version == 4: + super(RH, self).bring_interfaces_up( + ip, primary_interface, secondary_interface) + else: + # Secondary interface is not present in IPv6 configuration + self._bring_if_down(primary_interface) + self._bring_if_up(primary_interface, 'VIP') + + def has_ifup_all(self): + return False diff --git a/octavia/amphorae/backends/agent/api_server/plug.py b/octavia/amphorae/backends/agent/api_server/plug.py index 397abd7c24..196adb292f 100644 --- a/octavia/amphorae/backends/agent/api_server/plug.py +++ b/octavia/amphorae/backends/agent/api_server/plug.py @@ -15,7 +15,6 @@ import logging import os -import shutil import socket import stat import subprocess @@ -29,9 +28,8 @@ import pyroute2 import six from werkzeug import exceptions -from octavia.amphorae.backends.agent.api_server import util from octavia.common import constants as consts -from octavia.i18n import _LE, _LI +from octavia.i18n import _LI CONF = cfg.CONF @@ -48,6 +46,8 @@ template_vip = j2_env.get_template(ETH_X_VIP_CONF) class Plug(object): + def __init__(self, osutils): + self._osutils = osutils def plug_vip(self, vip, subnet_cidr, gateway, mac_address, mtu=None, vrrp_ip=None, host_routes=None): @@ -97,57 +97,24 @@ class Plug(object): secondary_interface = "{interface}:0".format( interface=primary_interface) - # We need to setup the netns network directory so that the ifup - # commands used here and in the startup scripts "sees" the right - # interfaces and scripts. - interface_file_path = util.get_network_interface_file( + interface_file_path = self._osutils.get_network_interface_file( primary_interface) - os.makedirs('/etc/netns/' + consts.AMPHORA_NAMESPACE) - shutil.copytree( - '/etc/network', - '/etc/netns/{}/network'.format(consts.AMPHORA_NAMESPACE), - symlinks=True, - ignore=shutil.ignore_patterns('eth0*', 'openssh*')) - name = '/etc/netns/{}/network/interfaces'.format( - consts.AMPHORA_NAMESPACE) - 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(name, flags, mode), 'w') as int_file: - int_file.write('auto lo\n') - int_file.write('iface lo inet loopback\n') - if not CONF.amphora_agent.agent_server_network_file: - int_file.write('source /etc/netns/{}/network/' - 'interfaces.d/*.cfg\n'.format( - consts.AMPHORA_NAMESPACE)) - # write interface file + self._osutils.create_netns_dir() - mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - - # If we are using a consolidated interfaces file, just append - # otherwise clear the per interface file as we are rewriting it - # TODO(johnsom): We need a way to clean out old interfaces records - if CONF.amphora_agent.agent_server_network_file: - flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND - else: - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC - - with os.fdopen(os.open(interface_file_path, flags, mode), - 'w') as text_file: - text = template_vip.render( - interface=primary_interface, - vip=vip, - vip_ipv6=ip.version is 6, - broadcast=broadcast, - netmask=netmask, - gateway=gateway, - mtu=mtu, - vrrp_ip=vrrp_ip, - vrrp_ipv6=vrrp_version is 6, - host_routes=render_host_routes, - ) - text_file.write(text) + self._osutils.write_interfaces_file() + self._osutils.write_vip_interface_file( + interface_file_path=interface_file_path, + primary_interface=primary_interface, + vip=vip, + ip=ip, + broadcast=broadcast, + netmask=netmask, + gateway=gateway, + mtu=mtu, + vrrp_ip=vrrp_ip, + vrrp_version=vrrp_version, + render_host_routes=render_host_routes) # Update the list of interfaces to add to the namespace # This is used in the amphora reboot case to re-establish the namespace @@ -172,60 +139,14 @@ class Plug(object): IFLA_IFNAME=primary_interface) # bring interfaces up - self._bring_if_down(primary_interface) - self._bring_if_down(secondary_interface) - self._bring_if_up(primary_interface, 'VIP') - self._bring_if_up(secondary_interface, 'VIP') + self._osutils.bring_interfaces_up( + ip, primary_interface, secondary_interface) return flask.make_response(flask.jsonify(dict( message="OK", details="VIP {vip} plugged on interface {interface}".format( vip=vip, interface=primary_interface))), 202) - def _generate_network_file_text(self, netns_interface, fixed_ips, mtu): - text = '' - if fixed_ips is None: - text = template_port.render(interface=netns_interface) - else: - for index, fixed_ip in enumerate(fixed_ips, -1): - if index == -1: - netns_ip_interface = netns_interface - else: - netns_ip_interface = "{int}:{ip}".format( - int=netns_interface, ip=index) - try: - ip_addr = fixed_ip['ip_address'] - cidr = fixed_ip['subnet_cidr'] - ip = ipaddress.ip_address( - ip_addr if six.text_type == type( - ip_addr) else six.u(ip_addr)) - network = ipaddress.ip_network( - cidr if six.text_type == type( - cidr) else six.u(cidr)) - broadcast = network.broadcast_address.exploded - netmask = (network.prefixlen if ip.version is 6 - else network.netmask.exploded) - host_routes = [] - for hr in fixed_ip.get('host_routes', []): - network = ipaddress.ip_network( - hr['destination'] if isinstance( - hr['destination'], six.text_type) else - six.u(hr['destination'])) - host_routes.append({'network': network, - 'gw': hr['nexthop']}) - except ValueError: - return flask.make_response(flask.jsonify(dict( - message="Invalid network IP")), 400) - new_text = template_port.render(interface=netns_ip_interface, - ipv6=ip.version is 6, - ip_address=ip.exploded, - broadcast=broadcast, - netmask=netmask, - mtu=mtu, - host_routes=host_routes) - text = '\n'.join([text, new_text]) - return text - def _check_ip_addresses(self, fixed_ips): if fixed_ips: for ip in fixed_ips: @@ -267,27 +188,13 @@ class Plug(object): 'namespace {2}').format(default_netns_interface, netns_interface, consts.AMPHORA_NAMESPACE)) - interface_file_path = util.get_network_interface_file(netns_interface) - - # write interface file - - # If we are using a consolidated interfaces file, just append - # otherwise clear the per interface file as we are rewriting it - # TODO(johnsom): We need a way to clean out old interfaces records - if CONF.amphora_agent.agent_server_network_file: - flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND - else: - 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(interface_file_path, flags, mode), - 'w') as text_file: - text = self._generate_network_file_text(netns_interface, - fixed_ips, - mtu) - text_file.write(text) + interface_file_path = self._osutils.get_network_interface_file( + netns_interface) + self._osutils.write_port_interface_file( + netns_interface=netns_interface, + fixed_ips=fixed_ips, + mtu=mtu, + interface_file_path=interface_file_path) # Update the list of interfaces to add to the namespace self._update_plugged_interfaces_file(netns_interface, mac_address) @@ -299,8 +206,8 @@ class Plug(object): net_ns_fd=consts.AMPHORA_NAMESPACE, IFLA_IFNAME=netns_interface) - self._bring_if_down(netns_interface) - self._bring_if_up(netns_interface, 'network') + self._osutils._bring_if_down(netns_interface) + self._osutils._bring_if_up(netns_interface, 'network') return flask.make_response(flask.jsonify(dict( message="OK", @@ -318,31 +225,6 @@ class Plug(object): response=flask.make_response(flask.jsonify(dict( details="No suitable network interface found")), 404)) - def _bring_if_up(self, interface, what): - # Note, we are not using pyroute2 for this as it is not /etc/netns - # aware. - cmd = ("ip netns exec {ns} ifup {params}".format( - ns=consts.AMPHORA_NAMESPACE, params=interface)) - try: - subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - LOG.error(_LE('Failed to if up {0} due to ' - 'error: {1}').format(interface, str(e))) - raise exceptions.HTTPException( - response=flask.make_response(flask.jsonify(dict( - message='Error plugging {0}'.format(what), - details=e.output)), 500)) - - def _bring_if_down(self, interface): - # Note, we are not using pyroute2 for this as it is not /etc/netns - # aware. - cmd = ("ip netns exec {ns} ifdown {params}".format( - ns=consts.AMPHORA_NAMESPACE, params=interface)) - try: - subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) - except subprocess.CalledProcessError: - pass - def _update_plugged_interfaces_file(self, interface, mac_address): # write interfaces to plugged_interfaces file and prevent duplicates plug_inf_file = consts.PLUGGED_INTERFACES diff --git a/octavia/amphorae/backends/agent/api_server/server.py b/octavia/amphorae/backends/agent/api_server/server.py index 62e0ccd37a..541759c495 100644 --- a/octavia/amphorae/backends/agent/api_server/server.py +++ b/octavia/amphorae/backends/agent/api_server/server.py @@ -22,6 +22,7 @@ from octavia.amphorae.backends.agent.api_server import amphora_info from octavia.amphorae.backends.agent.api_server import certificate_update 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 PATH_PREFIX = '/' + api_server.VERSION @@ -43,9 +44,11 @@ def register_app_error_handler(app): 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._plug = plug.Plug() + self._plug = plug.Plug(self._osutils) + self._amphora_info = amphora_info.AmphoraInfo(self._osutils) register_app_error_handler(self.app) @@ -119,10 +122,10 @@ class Server(object): return self._listener.delete_listener(listener_id) def get_details(self): - return amphora_info.compile_amphora_details() + return self._amphora_info.compile_amphora_details() def get_info(self): - return amphora_info.compile_amphora_info() + return self._amphora_info.compile_amphora_info() def get_all_listeners_status(self): return self._listener.get_all_listeners_status() @@ -179,4 +182,4 @@ class Server(object): return self._keepalived.manager_keepalived_service(action) def get_interface(self, ip_addr): - return amphora_info.get_interface(ip_addr) \ No newline at end of file + return self._amphora_info.get_interface(ip_addr) 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 a1684a6f55..83950a9757 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 @@ -16,7 +16,6 @@ #} # Generated by Octavia agent auto {{ interface }} {{ interface }}:0 - {%- if vrrp_ip %} iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static address {{ vrrp_ip }} @@ -31,6 +30,7 @@ up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} {%- endfor %} {%- else %} + iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }} {%- endif %} diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_ethX.conf.j2 new file mode 100644 index 0000000000..1b47b2a868 --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_ethX.conf.j2 @@ -0,0 +1,47 @@ +{# +# Copyright 2017 Red Hat, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +#} +# Generated by Octavia agent +NM_CONTROLLED="no" +DEVICE="{{ interface }}" +ONBOOT="yes" +TYPE="Ethernet" +USERCTL="yes" +{%- if ipv6 %} +IPV6INIT="yes" +{%- if mtu %} +IPV6_MTU="{{ mtu }}" +{%- endif %} +{%- if ip_address %} +IPV6_AUTOCONF="no" +IPV6ADDR="{{ ip_address }}" +{%- else %} +IPV6_AUTOCONF="yes" +{%- endif %} +{%- else %} +IPV6INIT="no" +{%- if mtu %} +MTU="{{ mtu }}" +{%- endif %} +{%- if ip_address %} +BOOTPROTO="static" +IPADDR="{{ ip_address }}" +NETMASK="{{ netmask }}" +{%- else %} +BOOTPROTO="dhcp" +PERSISTENT_DHCLIENT="1" +{%- endif %} +{%- endif %} + diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX.conf.j2 new file mode 100644 index 0000000000..29ef3fa49d --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX.conf.j2 @@ -0,0 +1,54 @@ +{# +# Copyright 2017 Red Hat, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +#} +# Generated by Octavia agent +NM_CONTROLLED="no" +DEVICE="{{ interface }}" +ONBOOT="yes" +TYPE="Ethernet" +USERCTL="yes" + +{%- if vrrp_ip %} +{%- if vrrp_ipv6 %} +IPV6INIT="yes" +IPV6_DEFROUTE="yes" +IPV6_AUTOCONF="no" +IPV6ADDR="{{ vrrp_ip }}/{{ prefix }}" +IPV6_DEFAULTGW="{{ gateway }}" +{%- if mtu %} +IPV6_MTU="{{ mtu }}" +{%- endif %} +{%- else %} {# not vrrp_ipv6 #} +BOOTPROTO="static" +IPADDR="{{ vrrp_ip }}" +NETMASK="{{ netmask }}" +GATEWAY="{{ gateway }}" +MTU="{{ mtu }}" +{%- endif %} {# end if vrrp_ipv6 #} +{%- else %} {# not vrrp_ip #} +{%- if vip_ipv6 %} +IPV6INIT="yes" +IPV6_DEFROUTE="yes" +IPV6_AUTOCONF="yes" +{%- else %} +BOOTPROTO="dhcp" +PERSISTENT_DHCLIENT="1" +{%- endif %} {# end if vip_ipv6 #} +{%- endif %} {# end if vrrp_ip #} + +{%- if vip_ipv6 %} +IPV6ADDR_SECONDARIES="{{ vip }}/{{ prefix }}" +{%- endif %} + diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX_alias.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX_alias.conf.j2 new file mode 100644 index 0000000000..42430f0a26 --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX_alias.conf.j2 @@ -0,0 +1,29 @@ +{# +# Copyright 2017 Red Hat, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +#} +# Generated by Octavia agent +NM_CONTROLLED="no" +DEVICE="{{ interface }}:0" +NAME="{{ interface }}:0" +ONBOOT="yes" +ARPCHECK="no" +IPV6INIT="no" +{%- if mtu %} +MTU="{{ mtu }}" +{%- endif %} +BOOTPROTO="static" +IPADDR="{{ vip }}" +NETMASK="{{ netmask }}" + diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 new file mode 100644 index 0000000000..490aa4d029 --- /dev/null +++ b/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 @@ -0,0 +1,20 @@ +{# +# Copyright 2017 Red Hat, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +#} +# Generated by Octavia agent +{%- for hr in host_routes %} +{{ hr.network }} via {{ hr.gw }} dev {{ interface }} +{%- endfor %} + diff --git a/octavia/amphorae/backends/agent/api_server/templates/systemd.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/systemd.conf.j2 index 739a9c285d..da3e6e5e7d 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/systemd.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/systemd.conf.j2 @@ -15,9 +15,13 @@ ExecStartPre=-/sbin/ip netns exec {{ amphora_nsname }} sysctl --system # We need the plugged_interfaces file sorted to join the host interfaces ExecStartPre=-/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 -ExecStartPre=-/bin/sh -c '/sbin/ip link | awk \'{getline n; print $0,n}\' | awk \'{sub(":","",$2)} {print $17 " " $2}\' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk \'{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}\'' +ExecStartPre=-/bin/sh -c '/sbin/ip link | awk \'{getline n; print $0,n}\' | awk \'{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }\' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk \'{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}\'' # Bring up all of the namespace interfaces +{%- if HasIFUPAll %} ExecStartPre=-/sbin/ip netns exec {{ amphora_nsname }} ifup -a +{%- else %} +ExecStartPre=-/bin/awk '{system("/sbin/ip netns exec amphora-haproxy ifup " $2)}' /var/lib/octavia/plugged_interfaces +{%- endif %} # ExecStart=/sbin/ip netns exec {{ amphora_nsname }} /usr/sbin/haproxy-systemd-wrapper -f {{ haproxy_cfg }} -p {{ haproxy_pid }} -L {{ peer_name }} $EXTRAOPTS ExecReload=/usr/sbin/haproxy -c -f {{ haproxy_cfg }} diff --git a/octavia/amphorae/backends/agent/api_server/templates/sysvinit.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/sysvinit.conf.j2 index 3038a203c7..f105ca4d7c 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/sysvinit.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/sysvinit.conf.j2 @@ -57,9 +57,13 @@ haproxy_start() # We need the plugged_interfaces file sorted to join the host interfaces sort -k 1 /var/lib/octavia/plugged_interfaces > /var/lib/octavia/plugged_interfaces.sorted || true # Assign the interfaces into the namespace with the appropriate name - ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} {print $17 " " $2}' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true + ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true # Bring up all of the namespace interfaces + {%- if HasIFUPAll %} ip netns exec {{ amphora_nsname }} ifup -a || true + {%- else %} + awk '{system("/sbin/ip netns exec amphora-haproxy ifup " $2)}' /var/lib/octavia/plugged_interfaces || true + {%- endif %} start-stop-daemon --start --pidfile "$PIDFILE" \ --exec $HAPROXY -- -f "$CONFIG" -D -p "$PIDFILE" \ diff --git a/octavia/amphorae/backends/agent/api_server/templates/upstart.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/upstart.conf.j2 index 3138704a04..da88dc8a53 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/upstart.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/upstart.conf.j2 @@ -39,9 +39,14 @@ pre-start script # interfaces sort -k 1 /var/lib/octavia/plugged_interfaces > /var/lib/octavia/plugged_interfaces.sorted || true # Assign the interfaces into the namespace with the appropriate name - ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} {print $17 " " $2}' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true + ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true # Bring up all of the namespace interfaces + {%- if HasIFUPAll %} ip netns exec {{ amphora_nsname }} ifup -a || true + {%- else %} + awk '{system("/sbin/ip netns exec amphora-haproxy ifup " $2)}' /var/lib/octavia/plugged_interfaces || true + {%- endif %} + end script script diff --git a/octavia/amphorae/backends/agent/api_server/util.py b/octavia/amphorae/backends/agent/api_server/util.py index 4d8f4e762b..48cc15e9b6 100644 --- a/octavia/amphorae/backends/agent/api_server/util.py +++ b/octavia/amphorae/backends/agent/api_server/util.py @@ -125,13 +125,6 @@ def is_listener_running(listener_id): os.path.join('/proc', get_haproxy_pid(listener_id))) -def get_network_interface_file(interface): - if CONF.amphora_agent.agent_server_network_file: - return CONF.amphora_agent.agent_server_network_file - return os.path.join(CONF.amphora_agent.agent_server_network_dir, - interface + '.cfg') - - 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 0ffb735f88..cca80bd28a 100644 --- a/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template +++ b/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template @@ -34,7 +34,9 @@ heartbeat_key = {{ heartbeat_key }} [amphora_agent] agent_server_ca = {{ agent_server_ca }} agent_server_cert = {{ agent_server_cert }} +{% if agent_server_network_dir -%} agent_server_network_dir = {{ agent_server_network_dir }} +{% endif -%} {% if agent_server_network_file -%} agent_server_network_file = {{ agent_server_network_file }} {% endif -%} diff --git a/octavia/common/config.py b/octavia/common/config.py index f34ccfb0da..7744f19651 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -69,8 +69,6 @@ amphora_agent_opts = [ help=_("The server certificate for the agent.py server " "to use")), cfg.StrOpt('agent_server_network_dir', - default='/etc/netns/{}/network/interfaces.d/'.format( - constants.AMPHORA_NAMESPACE), help=_("The directory where new network interfaces " "are located")), cfg.StrOpt('agent_server_network_file', diff --git a/octavia/common/constants.py b/octavia/common/constants.py index 9a4ee471c8..34b47ffc1e 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -366,3 +366,8 @@ KEEPALIVED_UPSTART = 'octavia-keepalived.conf' # Authentication KEYSTONE = 'keystone' NOAUTH = 'noauth' + +UBUNTU_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/network/interfaces.d/' +RH_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/sysconfig/network-scripts/' +UBUNTU = 'ubuntu' +CENTOS = 'centos' diff --git a/octavia/common/exceptions.py b/octavia/common/exceptions.py index 2bbf8d7b30..e4d660e93e 100644 --- a/octavia/common/exceptions.py +++ b/octavia/common/exceptions.py @@ -237,6 +237,10 @@ class ServerGroupObjectDeleteException(OctaviaException): message = _('Failed to delete server group object.') +class InvalidAmphoraOperatingSystem(OctaviaException): + message = _('Invalid amphora operating system: %(os_name)s') + + class QuotaException(APIException): msg = _('Quota has been met.') code = 403 diff --git a/octavia/common/utils.py b/octavia/common/utils.py index b3f291ef4e..73f3f85ec2 100644 --- a/octavia/common/utils.py +++ b/octavia/common/utils.py @@ -74,6 +74,10 @@ def ip_port_str(ip_address, port): return "[{ip}]:{port}".format(ip=ip, port=port) +def netmask_to_prefix(netmask): + return netaddr.IPAddress(netmask).netmask_bits() + + class exception_logger(object): """Wrap a function and log raised exception diff --git a/octavia/tests/common/utils.py b/octavia/tests/common/utils.py index fd12650611..485b1db189 100644 --- a/octavia/tests/common/utils.py +++ b/octavia/tests/common/utils.py @@ -28,6 +28,9 @@ class OpenFixture(fixtures.Fixture): def _setUp(self): self.mock_open = mock.mock_open(read_data=self.contents) + # work around for https://bugs.python.org/issue21258 + self.mock_open.return_value.__iter__ = ( + lambda self: iter(self.readline, '')) self._orig_open = open def replacement_open(name, *args, **kwargs): 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 3bf8fd29c7..2bbb4b3eb5 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 @@ -22,6 +22,7 @@ import subprocess import mock import netifaces from oslo_config import fixture as oslo_fixture +from oslo_utils import uuidutils import six from octavia.amphorae.backends.agent import api_server @@ -44,55 +45,53 @@ class TestServerTestCase(base.TestCase): def setUp(self): super(TestServerTestCase, self).setUp() + with mock.patch('platform.linux_distribution', + return_value=['Ubuntu', 'Foo', 'Bar']): + self.ubuntu_test_server = server.Server() + self.ubuntu_app = self.ubuntu_test_server.app.test_client() - self.test_server = server.Server() - self.app = self.test_server.app.test_client() + with mock.patch('platform.linux_distribution', + return_value=['centos', 'Foo', 'Bar']): + self.centos_test_server = server.Server() + self.centos_app = self.centos_test_server.app.test_client() - conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF)) - conf.config(group="haproxy_amphora", base_path='/var/lib/octavia') + self.conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF)) + self.conf.config(group="haproxy_amphora", base_path='/var/lib/octavia') @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSTEMD) - @mock.patch('os.path.exists') - @mock.patch('os.makedirs') - @mock.patch('os.rename') - @mock.patch('subprocess.check_output') - @mock.patch('os.remove') - def test_haproxy_systemd(self, mock_remove, mock_subprocess, mock_rename, - mock_makedirs, mock_exists, mock_init_system): - self._test_haproxy(mock_remove, mock_subprocess, mock_rename, - mock_makedirs, mock_exists, mock_init_system, - consts.INIT_SYSTEMD) + def test_ubuntu_haproxy_systemd(self, mock_init_system): + self._test_haproxy(consts.INIT_SYSTEMD, consts.UBUNTU, + mock_init_system) + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_os_init_system', return_value=consts.INIT_SYSTEMD) + def test_centos_haproxy_systemd(self, mock_init_system): + self._test_haproxy(consts.INIT_SYSTEMD, consts.CENTOS, + mock_init_system) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSVINIT) - @mock.patch('os.path.exists') - @mock.patch('os.makedirs') - @mock.patch('os.rename') - @mock.patch('subprocess.check_output') - @mock.patch('os.remove') - def test_haproxy_sysvinit(self, mock_remove, mock_subprocess, mock_rename, - mock_makedirs, mock_exists, mock_init_system): - self._test_haproxy(mock_remove, mock_subprocess, mock_rename, - mock_makedirs, mock_exists, mock_init_system, - consts.INIT_SYSVINIT) + def test_ubuntu_haproxy_sysvinit(self, mock_init_system): + self._test_haproxy(consts.INIT_SYSVINIT, consts.UBUNTU, + mock_init_system) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_UPSTART) + def test_ubuntu_haproxy_upstart(self, mock_init_system): + self._test_haproxy(consts.INIT_UPSTART, consts.UBUNTU, + mock_init_system) + @mock.patch('os.path.exists') @mock.patch('os.makedirs') @mock.patch('os.rename') @mock.patch('subprocess.check_output') @mock.patch('os.remove') - def test_haproxy_upstart(self, mock_remove, mock_subprocess, mock_rename, - mock_makedirs, mock_exists, mock_init_system): - self._test_haproxy(mock_remove, mock_subprocess, mock_rename, - mock_makedirs, mock_exists, mock_init_system, - consts.INIT_UPSTART) + def _test_haproxy(self, init_system, distro, mock_init_system, + mock_remove, mock_subprocess, mock_rename, + mock_makedirs, mock_exists): - def _test_haproxy(self, mock_remove, mock_subprocess, mock_rename, - mock_makedirs, mock_exists, mock_init_system, - init_system): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mock_exists.return_value = True @@ -103,9 +102,14 @@ class TestServerTestCase(base.TestCase): with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', - data='test') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') mode = stat.S_IRUSR | stat.S_IWUSR mock_open.assert_called_with(file_name, flags, mode) mock_fdopen.assert_called_with(123, 'wb') @@ -137,9 +141,14 @@ class TestServerTestCase(base.TestCase): m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open m.side_effect = IOError() # open crashes with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): - rv = self.app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', - data='test') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') self.assertEqual(500, rv.status_code) # check if files get created @@ -158,9 +167,15 @@ class TestServerTestCase(base.TestCase): with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', - data='test') + + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') self.assertEqual(202, rv.status_code) if init_system == consts.INIT_SYSTEMD: @@ -183,9 +198,14 @@ class TestServerTestCase(base.TestCase): with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', - data='test') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') self.assertEqual(400, rv.status_code) self.assertEqual( {'message': 'Invalid request', u'details': u'random error'}, @@ -208,17 +228,34 @@ class TestServerTestCase(base.TestCase): with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.put('/' + api_server.VERSION + - '/listeners/amp_123/123/haproxy', - data='test') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') + elif distro == consts.CENTOS: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/amp_123/123/haproxy', + data='test') self.assertEqual(500, rv.status_code) + def test_ubuntu_start(self): + self._test_start(consts.UBUNTU) + + def test_centos_start(self): + self._test_start(consts.CENTOS) + @mock.patch('os.path.exists') @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' 'vrrp_check_script_update') @mock.patch('subprocess.check_output') - def test_start(self, mock_subprocess, mock_vrrp, mock_exists): - rv = self.app.put('/' + api_server.VERSION + '/listeners/123/error') + def _test_start(self, distro, mock_subprocess, mock_vrrp, mock_exists): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/error') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/error') self.assertEqual(400, rv.status_code) self.assertEqual( {'message': 'Invalid Request', @@ -226,7 +263,12 @@ class TestServerTestCase(base.TestCase): json.loads(rv.data.decode('utf-8'))) mock_exists.return_value = False - rv = self.app.put('/' + api_server.VERSION + '/listeners/123/start') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/start') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/start') self.assertEqual(404, rv.status_code) self.assertEqual( {'message': 'Listener Not Found', @@ -235,7 +277,12 @@ class TestServerTestCase(base.TestCase): mock_exists.assert_called_with('/var/lib/octavia/123/haproxy.cfg') mock_exists.return_value = True - rv = self.app.put('/' + api_server.VERSION + '/listeners/123/start') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/start') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/start') self.assertEqual(202, rv.status_code) self.assertEqual( {'message': 'OK', @@ -248,7 +295,12 @@ class TestServerTestCase(base.TestCase): mock_exists.return_value = True mock_subprocess.side_effect = subprocess.CalledProcessError( 7, 'test', RANDOM_ERROR) - rv = self.app.put('/' + api_server.VERSION + '/listeners/123/start') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/start') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/start') self.assertEqual(500, rv.status_code) self.assertEqual( { @@ -258,19 +310,32 @@ class TestServerTestCase(base.TestCase): mock_subprocess.assert_called_with( ['/usr/sbin/service', 'haproxy-123', 'start'], stderr=-2) + def test_ubuntu_reload(self): + self._test_reload(consts.UBUNTU) + + def test_centos_reload(self): + self._test_reload(consts.CENTOS) + @mock.patch('os.path.exists') @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' 'vrrp_check_script_update') @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' '_check_haproxy_status') @mock.patch('subprocess.check_output') - def test_reload(self, mock_subprocess, mock_haproxy_status, - mock_vrrp, mock_exists): + def _test_reload(self, distro, mock_subprocess, mock_haproxy_status, + mock_vrrp, mock_exists): + + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) # Process running so reload mock_exists.return_value = True mock_haproxy_status.return_value = consts.ACTIVE - rv = self.app.put('/' + api_server.VERSION + '/listeners/123/reload') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/reload') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/reload') self.assertEqual(202, rv.status_code) self.assertEqual( {'message': 'OK', @@ -282,7 +347,12 @@ class TestServerTestCase(base.TestCase): # Process not running so start mock_exists.return_value = True mock_haproxy_status.return_value = consts.OFFLINE - rv = self.app.put('/' + api_server.VERSION + '/listeners/123/reload') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/reload') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/reload') self.assertEqual(202, rv.status_code) self.assertEqual( {'message': 'OK', @@ -292,9 +362,16 @@ class TestServerTestCase(base.TestCase): mock_subprocess.assert_called_with( ['/usr/sbin/service', 'haproxy-123', 'start'], stderr=-2) + def test_ubuntu_info(self): + self._test_info(consts.UBUNTU) + + def test_centos_info(self): + self._test_info(consts.CENTOS) + @mock.patch('socket.gethostname') @mock.patch('subprocess.check_output') - def test_info(self, mock_subbprocess, mock_hostname): + def _test_info(self, distro, mock_subbprocess, mock_hostname): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) mock_hostname.side_effect = ['test-host'] mock_subbprocess.side_effect = [ """Package: haproxy @@ -304,66 +381,62 @@ class TestServerTestCase(base.TestCase): Installed-Size: 803 Maintainer: Ubuntu Developers Architecture: amd64 - Version: 1.4.24-2 + Version: 9.9.99-9 """] - rv = self.app.get('/' + api_server.VERSION + '/info') + + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + '/info') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + '/info') self.assertEqual(200, rv.status_code) self.assertEqual(dict( api_version='0.5', - haproxy_version='1.4.24-2', + haproxy_version='9.9.99-9', hostname='test-host'), json.loads(rv.data.decode('utf-8'))) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSTEMD) - @mock.patch('os.path.exists') - @mock.patch('subprocess.check_output') - @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + - 'get_haproxy_pid') - @mock.patch('shutil.rmtree') - @mock.patch('os.remove') - def test_delete_listener_systemd(self, mock_remove, mock_rmtree, mock_pid, - mock_check_output, mock_exists, - mock_init_system): - self._test_delete_listener(mock_remove, mock_rmtree, mock_pid, - mock_check_output, mock_exists, - consts.INIT_SYSTEMD) + def test_delete_ubuntu_listener_systemd(self, mock_init_system): + self._test_delete_listener(consts.INIT_SYSTEMD, consts.UBUNTU, + mock_init_system) + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_os_init_system', return_value=consts.INIT_SYSTEMD) + def test_delete_centos_listener_systemd(self, mock_init_system): + self._test_delete_listener(consts.INIT_SYSTEMD, consts.CENTOS, + mock_init_system) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSVINIT) - @mock.patch('os.path.exists') - @mock.patch('subprocess.check_output') - @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + - 'get_haproxy_pid') - @mock.patch('shutil.rmtree') - @mock.patch('os.remove') - def test_delete_listener_sysvinit(self, mock_remove, mock_rmtree, mock_pid, - mock_check_output, mock_exists, - mock_init_system): - self._test_delete_listener(mock_remove, mock_rmtree, mock_pid, - mock_check_output, mock_exists, - consts.INIT_SYSVINIT) + def test_delete_ubuntu_listener_sysvinit(self, mock_init_system): + self._test_delete_listener(consts.INIT_SYSVINIT, consts.UBUNTU, + mock_init_system) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_UPSTART) + def test_delete_ubuntu_listener_upstart(self, mock_init_system): + self._test_delete_listener(consts.INIT_UPSTART, consts.UBUNTU, + mock_init_system) + @mock.patch('os.path.exists') @mock.patch('subprocess.check_output') @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_haproxy_pid') @mock.patch('shutil.rmtree') @mock.patch('os.remove') - def test_delete_listener_upstart(self, mock_remove, mock_rmtree, mock_pid, - mock_check_output, mock_exists, - mock_init_system): - self._test_delete_listener(mock_remove, mock_rmtree, mock_pid, - mock_check_output, mock_exists, - consts.INIT_UPSTART) - - def _test_delete_listener(self, mock_remove, mock_rmtree, mock_pid, - mock_check_output, mock_exists, init_system): + def _test_delete_listener(self, init_system, distro, mock_init_system, + mock_remove, mock_rmtree, mock_pid, + mock_check_output, mock_exists): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) mock_exists.return_value = False - rv = self.app.delete('/' + api_server.VERSION + '/listeners/123') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.delete('/' + api_server.VERSION + + '/listeners/123') + elif distro == consts.CENTOS: + rv = self.centos_app.delete('/' + api_server.VERSION + + '/listeners/123') self.assertEqual(404, rv.status_code) self.assertEqual( {'message': 'Listener Not Found', @@ -373,7 +446,12 @@ class TestServerTestCase(base.TestCase): # service is stopped + no upstart script mock_exists.side_effect = [True, False, False] - rv = self.app.delete('/' + api_server.VERSION + '/listeners/123') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.delete('/' + api_server.VERSION + + '/listeners/123') + elif distro == consts.CENTOS: + rv = self.centos_app.delete('/' + api_server.VERSION + + '/listeners/123') self.assertEqual(200, rv.status_code) self.assertEqual({u'message': u'OK'}, json.loads(rv.data.decode('utf-8'))) @@ -395,7 +473,12 @@ class TestServerTestCase(base.TestCase): # service is stopped + upstart script mock_exists.side_effect = [True, False, True] - rv = self.app.delete('/' + api_server.VERSION + '/listeners/123') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.delete('/' + api_server.VERSION + + '/listeners/123') + elif distro == consts.CENTOS: + rv = self.centos_app.delete('/' + api_server.VERSION + + '/listeners/123') self.assertEqual(200, rv.status_code) self.assertEqual({u'message': u'OK'}, json.loads(rv.data.decode('utf-8'))) @@ -415,7 +498,12 @@ class TestServerTestCase(base.TestCase): # service is running + upstart script mock_exists.side_effect = [True, True, True, True] mock_pid.return_value = '456' - rv = self.app.delete('/' + api_server.VERSION + '/listeners/123') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.delete('/' + api_server.VERSION + + '/listeners/123') + elif distro == consts.CENTOS: + rv = self.centos_app.delete('/' + api_server.VERSION + + '/listeners/123') self.assertEqual(200, rv.status_code) self.assertEqual({u'message': u'OK'}, json.loads(rv.data.decode('utf-8'))) @@ -441,7 +529,12 @@ class TestServerTestCase(base.TestCase): mock_exists.side_effect = [True, True, True] mock_check_output.side_effect = subprocess.CalledProcessError( 7, 'test', RANDOM_ERROR) - rv = self.app.delete('/' + api_server.VERSION + '/listeners/123') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.delete('/' + api_server.VERSION + + '/listeners/123') + elif distro == consts.CENTOS: + rv = self.centos_app.delete('/' + api_server.VERSION + + '/listeners/123') self.assertEqual(500, rv.status_code) self.assertEqual( {'details': 'random error', 'message': 'Error stopping haproxy'}, @@ -449,11 +542,25 @@ class TestServerTestCase(base.TestCase): # that's the last call before exception mock_exists.assert_called_with('/proc/456') + def test_ubuntu_get_haproxy(self): + self._test_get_haproxy(consts.UBUNTU) + + def test_centos_get_haproxy(self): + self._test_get_haproxy(consts.CENTOS) + @mock.patch('os.path.exists') - def test_get_haproxy(self, mock_exists): + def _test_get_haproxy(self, distro, mock_exists): + + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) + CONTENT = "bibble\nbibble" mock_exists.side_effect = [False] - rv = self.app.get('/' + api_server.VERSION + '/listeners/123/haproxy') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/listeners/123/haproxy') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/listeners/123/haproxy') self.assertEqual(404, rv.status_code) mock_exists.side_effect = [True] @@ -461,23 +568,39 @@ class TestServerTestCase(base.TestCase): path = util.config_path('123') self.useFixture(test_utils.OpenFixture(path, CONTENT)) - rv = self.app.get('/' + api_server.VERSION + - '/listeners/123/haproxy') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/listeners/123/haproxy') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/listeners/123/haproxy') self.assertEqual(200, rv.status_code) self.assertEqual(six.b(CONTENT), rv.data) self.assertEqual('text/plain; charset=utf-8', rv.headers['Content-Type']) + def test_ubuntu_get_all_listeners(self): + self._test_get_all_listeners(consts.UBUNTU) + + def test_get_all_listeners(self): + self._test_get_all_listeners(consts.CENTOS) + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_listeners') @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' '_check_listener_status') @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' '_parse_haproxy_file') - def test_get_all_listeners(self, mock_parse, mock_status, mock_listener): + def _test_get_all_listeners(self, distro, mock_parse, mock_status, + mock_listener): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) + # no listeners mock_listener.side_effect = [[]] - rv = self.app.get('/' + api_server.VERSION + '/listeners') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + '/listeners') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + '/listeners') self.assertEqual(200, rv.status_code) self.assertFalse(json.loads(rv.data.decode('utf-8'))) @@ -486,7 +609,10 @@ class TestServerTestCase(base.TestCase): mock_listener.side_effect = [['123']] mock_parse.side_effect = [{'mode': 'test'}] mock_status.side_effect = [consts.ACTIVE] - rv = self.app.get('/' + api_server.VERSION + '/listeners') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + '/listeners') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + '/listeners') self.assertEqual(200, rv.status_code) self.assertEqual( @@ -497,7 +623,10 @@ class TestServerTestCase(base.TestCase): mock_listener.side_effect = [['123', '456']] mock_parse.side_effect = [{'mode': 'test'}, {'mode': 'http'}] mock_status.side_effect = [consts.ACTIVE, consts.ERROR] - rv = self.app.get('/' + api_server.VERSION + '/listeners') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + '/listeners') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + '/listeners') self.assertEqual(200, rv.status_code) self.assertEqual( @@ -505,17 +634,29 @@ class TestServerTestCase(base.TestCase): {'status': consts.ERROR, 'type': '', 'uuid': '456'}], json.loads(rv.data.decode('utf-8'))) + def test_ubuntu_get_listener(self): + self._test_get_listener(consts.UBUNTU) + + def test_centos_get_listener(self): + self._test_get_listener(consts.CENTOS) + @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' '_check_listener_status') @mock.patch('octavia.amphorae.backends.agent.api_server.listener.Listener.' '_parse_haproxy_file') @mock.patch('octavia.amphorae.backends.utils.haproxy_query.HAProxyQuery') @mock.patch('os.path.exists') - def test_get_listener(self, mock_exists, mock_query, mock_parse, - mock_status): + def _test_get_listener(self, distro, mock_exists, mock_query, mock_parse, + mock_status): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) # Listener not found mock_exists.side_effect = [False] - rv = self.app.get('/' + api_server.VERSION + '/listeners/123') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/listeners/123') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/listeners/123') self.assertEqual(404, rv.status_code) self.assertEqual( {'message': 'Listener Not Found', @@ -526,7 +667,12 @@ class TestServerTestCase(base.TestCase): mock_parse.side_effect = [dict(mode='test')] mock_status.side_effect = [consts.ERROR] mock_exists.side_effect = [True] - rv = self.app.get('/' + api_server.VERSION + '/listeners/123') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/listeners/123') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/listeners/123') self.assertEqual(200, rv.status_code) self.assertEqual(dict( status=consts.ERROR, @@ -546,7 +692,12 @@ class TestServerTestCase(base.TestCase): 'members': [ {'id-34833': 'DOWN'}, {'id-34836': 'DOWN'}]}}] - rv = self.app.get('/' + api_server.VERSION + '/listeners/123') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/listeners/123') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/listeners/123') self.assertEqual(200, rv.status_code) self.assertEqual(dict( status=consts.ACTIVE, @@ -560,12 +711,23 @@ class TestServerTestCase(base.TestCase): {u'id-34836': u'DOWN'}])]), json.loads(rv.data.decode('utf-8'))) + def test_ubuntu_delete_cert(self): + self._test_delete_cert(consts.UBUNTU) + + def test_centos_delete_cert(self): + self._test_delete_cert(consts.CENTOS) + @mock.patch('os.path.exists') @mock.patch('os.remove') - def test_delete_cert(self, mock_remove, mock_exists): + def _test_delete_cert(self, distro, mock_remove, mock_exists): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) mock_exists.side_effect = [False] - rv = self.app.delete('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.delete('/' + api_server.VERSION + + '/listeners/123/certificates/test.pem') + elif distro == consts.CENTOS: + rv = self.centos_app.delete('/' + api_server.VERSION + + '/listeners/123/certificates/test.pem') self.assertEqual(404, rv.status_code) self.assertEqual(dict( details='No certificate with filename: test.pem', @@ -576,26 +738,46 @@ class TestServerTestCase(base.TestCase): # wrong file name mock_exists.side_effect = [True] - rv = self.app.put('/' + api_server.VERSION + - '/listeners/123/certificates/test.bla', - data='TestTest') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.delete('/' + api_server.VERSION + + '/listeners/123/certificates/test.bla') + elif distro == consts.CENTOS: + rv = self.centos_app.delete('/' + api_server.VERSION + + '/listeners/123/certificates/test.bla') self.assertEqual(400, rv.status_code) mock_exists.side_effect = [True] - rv = self.app.delete('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.delete('/' + api_server.VERSION + + '/listeners/123/certificates/test.pem') + elif distro == consts.CENTOS: + rv = self.centos_app.delete('/' + api_server.VERSION + + '/listeners/123/certificates/test.pem') self.assertEqual(200, rv.status_code) self.assertEqual(OK, json.loads(rv.data.decode('utf-8'))) mock_remove.assert_called_once_with( '/var/lib/octavia/certs/123/test.pem') + def test_ubuntu_get_certificate_md5(self): + self._test_get_certificate_md5(consts.UBUNTU) + + def test_centos_get_certificate_md5(self): + self._test_get_certificate_md5(consts.CENTOS) + @mock.patch('os.path.exists') - def test_get_certificate_md5(self, mock_exists): + def _test_get_certificate_md5(self, distro, mock_exists): + + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) CONTENT = "TestTest" mock_exists.side_effect = [False] - rv = self.app.get('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/listeners/123/certificates/test.pem') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/listeners/123/certificates/test.pem') self.assertEqual(404, rv.status_code) self.assertEqual(dict( details='No certificate with filename: test.pem', @@ -605,40 +787,76 @@ class TestServerTestCase(base.TestCase): # wrong file name mock_exists.side_effect = [True] - rv = self.app.put('/' + api_server.VERSION + - '/listeners/123/certificates/test.bla', - data='TestTest') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/certificates/test.bla', + data='TestTest') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/certificates/test.bla', + data='TestTest') self.assertEqual(400, rv.status_code) mock_exists.return_value = True mock_exists.side_effect = None - path = self.test_server._listener._cert_file_path('123', 'test.pem') + if distro == consts.UBUNTU: + path = self.ubuntu_test_server._listener._cert_file_path( + '123', 'test.pem') + elif distro == consts.CENTOS: + path = self.centos_test_server._listener._cert_file_path( + '123', 'test.pem') self.useFixture(test_utils.OpenFixture(path, CONTENT)) - - rv = self.app.get('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/listeners/123/certificates/test.pem') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/listeners/123/certificates/test.pem') self.assertEqual(200, rv.status_code) self.assertEqual(dict(md5sum=hashlib.md5(six.b(CONTENT)).hexdigest()), json.loads(rv.data.decode('utf-8'))) + def test_ubuntu_upload_certificate_md5(self): + self._test_upload_certificate_md5(consts.UBUNTU) + + def test_centos_upload_certificate_md5(self): + self._test_upload_certificate_md5(consts.CENTOS) + @mock.patch('os.path.exists') @mock.patch('os.makedirs') - def test_upload_certificate_md5(self, mock_makedir, mock_exists): + def _test_upload_certificate_md5(self, distro, mock_makedir, mock_exists): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) # wrong file name - rv = self.app.put('/' + api_server.VERSION + - '/listeners/123/certificates/test.bla', - data='TestTest') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/certificates/test.bla', + data='TestTest') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/certificates/test.bla', + data='TestTest') self.assertEqual(400, rv.status_code) mock_exists.return_value = True - path = self.test_server._listener._cert_file_path('123', 'test.pem') + if distro == consts.UBUNTU: + path = self.ubuntu_test_server._listener._cert_file_path( + '123', 'test.pem') + elif distro == consts.CENTOS: + path = self.centos_test_server._listener._cert_file_path( + '123', 'test.pem') + m = self.useFixture(test_utils.OpenFixture(path)).mock_open with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): - rv = self.app.put('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem', - data='TestTest') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/certificates/' + 'test.pem', data='TestTest') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/certificates/' + 'test.pem', data='TestTest') self.assertEqual(200, rv.status_code) self.assertEqual(OK, json.loads(rv.data.decode('utf-8'))) handle = m() @@ -648,29 +866,55 @@ class TestServerTestCase(base.TestCase): m = self.useFixture(test_utils.OpenFixture(path)).mock_open with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): - rv = self.app.put('/' + api_server.VERSION + - '/listeners/123/certificates/test.pem', - data='TestTest') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/listeners/123/certificates/' + 'test.pem', data='TestTest') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/listeners/123/certificates/' + 'test.pem', data='TestTest') self.assertEqual(200, rv.status_code) self.assertEqual(OK, json.loads(rv.data.decode('utf-8'))) handle = m() handle.write.assert_called_once_with(six.b('TestTest')) mock_makedir.assert_called_once_with('/var/lib/octavia/certs/123') - def test_upload_server_certificate(self): + def test_ubuntu_upload_server_certificate(self): + self._test_upload_server_certificate(consts.UBUNTU) + + def test_centos_upload_server_certificate(self): + self._test_upload_server_certificate(consts.CENTOS) + + def _test_upload_server_certificate(self, distro): certificate_update.BUFFER = 5 # test the while loop path = '/etc/octavia/certs/server.pem' m = self.useFixture(test_utils.OpenFixture(path)).mock_open with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): - rv = self.app.put('/' + api_server.VERSION + - '/certificate', - data='TestTest') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/certificate', data='TestTest') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/certificate', data='TestTest') self.assertEqual(202, rv.status_code) self.assertEqual(OK, json.loads(rv.data.decode('utf-8'))) handle = m() handle.write.assert_any_call(six.b('TestT')) handle.write.assert_any_call(six.b('est')) + def test_ubuntu_plug_network(self): + self._test_plug_network(consts.UBUNTU) + self.conf.config(group="amphora_agent", + agent_server_network_file="/path/to/interfaces_file") + self._test_plug_network(consts.UBUNTU) + + def test_centos_plug_network(self): + self._test_plug_network(consts.CENTOS) + self.conf.config(group="amphora_agent", + agent_server_network_file="/path/to/interfaces_file") + self._test_plug_network(consts.CENTOS) + @mock.patch('netifaces.interfaces') @mock.patch('netifaces.ifaddresses') @mock.patch('pyroute2.IPRoute') @@ -678,8 +922,10 @@ class TestServerTestCase(base.TestCase): @mock.patch('subprocess.check_output') @mock.patch('octavia.amphorae.backends.agent.api_server.' 'plug.Plug._netns_interface_exists') - def test_plug_network(self, mock_int_exists, mock_check_output, mock_netns, - mock_pyroute2, mock_ifaddress, mock_interfaces): + def _test_plug_network(self, distro, mock_int_exists, mock_check_output, + mock_netns, mock_pyroute2, mock_ifaddress, + mock_interfaces): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) port_info = {'mac_address': '123'} test_int_num = random.randint(0, 9999) @@ -691,9 +937,16 @@ class TestServerTestCase(base.TestCase): # Interface already plugged mock_int_exists.return_value = True - rv = self.app.post('/' + api_server.VERSION + "/plug/network", - content_type='application/json', - data=json.dumps(port_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) self.assertEqual(409, rv.status_code) self.assertEqual(dict(message="Interface already exists"), json.loads(rv.data.decode('utf-8'))) @@ -701,9 +954,16 @@ class TestServerTestCase(base.TestCase): # No interface at all mock_interfaces.side_effect = [[]] - rv = self.app.post('/' + api_server.VERSION + "/plug/network", - content_type='application/json', - data=json.dumps(port_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) self.assertEqual(404, rv.status_code) self.assertEqual(dict(details="No suitable network interface found"), json.loads(rv.data.decode('utf-8'))) @@ -711,9 +971,16 @@ class TestServerTestCase(base.TestCase): # No interface down mock_interfaces.side_effect = [['blah']] mock_ifaddress.side_effect = [[netifaces.AF_INET]] - rv = self.app.post('/' + api_server.VERSION + "/plug/network", - content_type='application/json', - data=json.dumps(port_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) self.assertEqual(404, rv.status_code) self.assertEqual(dict(details="No suitable network interface found"), json.loads(rv.data.decode('utf-8'))) @@ -724,17 +991,38 @@ class TestServerTestCase(base.TestCase): mock_ifaddress.side_effect = [[netifaces.AF_LINK], {netifaces.AF_LINK: [{'addr': '123'}]}] - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - file_name = '/etc/netns/{0}/network/interfaces.d/eth{1}.cfg'.format( - consts.AMPHORA_NAMESPACE, test_int_num) + + if self.conf.conf.amphora_agent.agent_server_network_file: + file_name = self.conf.conf.amphora_agent.agent_server_network_file + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + + elif distro == consts.UBUNTU: + file_name = ('/etc/netns/{0}/network/interfaces.d/' + 'eth{1}.cfg'.format(consts.AMPHORA_NAMESPACE, + test_int_num)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + + elif distro == consts.CENTOS: + file_name = ('/etc/netns/{0}/sysconfig/network-scripts/' + 'ifcfg-eth{1}'.format(consts.AMPHORA_NAMESPACE, + test_int_num)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.post('/' + api_server.VERSION + "/plug/network", - content_type='application/json', - data=json.dumps(port_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) self.assertEqual(202, rv.status_code) mock_open.assert_any_call(file_name, flags, mode) @@ -747,12 +1035,24 @@ class TestServerTestCase(base.TestCase): mock_fdopen.assert_any_call(123, 'r+') handle = m() - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto eth' + test_int_num + - '\niface eth' + test_int_num + ' inet dhcp\n' - 'auto eth' + test_int_num + ':0\n' - 'iface eth' + test_int_num + ':0 inet6 auto\n') + if distro == consts.UBUNTU: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'auto eth{int}\n' + 'iface eth{int} inet dhcp\n' + 'auto eth{int}:0\n' + 'iface eth{int}:0 inet6 auto\n'.format(int=test_int_num)) + elif distro == consts.CENTOS: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'NM_CONTROLLED="no"\n' + 'DEVICE="eth{int}"\n' + 'ONBOOT="yes"\n' + 'TYPE="Ethernet"\n' + 'USERCTL="yes"\n' + 'IPV6INIT="no"\n' + 'BOOTPROTO="dhcp"\n' + 'PERSISTENT_DHCLIENT="1"\n'.format(int=test_int_num)) mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, 'ifup', 'eth' + test_int_num], stderr=-2) @@ -764,17 +1064,36 @@ class TestServerTestCase(base.TestCase): mock_ifaddress.side_effect = [[netifaces.AF_LINK], {netifaces.AF_LINK: [{'addr': '123'}]}] - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - file_name = '/etc/netns/{0}/network/interfaces.d/eth{1}.cfg'.format( - consts.AMPHORA_NAMESPACE, test_int_num) + + if self.conf.conf.amphora_agent.agent_server_network_file: + file_name = self.conf.conf.amphora_agent.agent_server_network_file + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + elif distro == consts.UBUNTU: + file_name = ('/etc/netns/{0}/network/interfaces.d/' + 'eth{1}.cfg'.format(consts.AMPHORA_NAMESPACE, + test_int_num)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + elif distro == consts.CENTOS: + file_name = ('/etc/netns/{0}/sysconfig/network-scripts/' + 'ifcfg-eth{1}'.format(consts.AMPHORA_NAMESPACE, + test_int_num)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.post('/' + api_server.VERSION + "/plug/network", - content_type='application/json', - data=json.dumps(port_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) self.assertEqual(202, rv.status_code) mock_open.assert_any_call(file_name, flags, mode) @@ -787,12 +1106,27 @@ class TestServerTestCase(base.TestCase): mock_fdopen.assert_any_call(123, 'r+') handle = m() - handle.write.assert_any_call( - '\n\n# Generated by Octavia agent\n' - 'auto eth' + test_int_num + - '\niface eth' + test_int_num + ' inet static\n' + - 'address 10.0.0.5\nbroadcast 10.0.0.255\n' + - 'netmask 255.255.255.0\nmtu 1450\n') + if distro == consts.UBUNTU: + handle.write.assert_any_call( + '\n\n# Generated by Octavia agent\n' + 'auto eth{int}\n' + '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)) + elif distro == consts.CENTOS: + handle.write.assert_any_call( + '\n\n# Generated by Octavia agent\n' + 'NM_CONTROLLED="no"\n' + 'DEVICE="eth{int}"\n' + 'ONBOOT="yes"\n' + 'TYPE="Ethernet"\n' + 'USERCTL="yes"\n' + 'IPV6INIT="no"\n' + 'MTU="1450"\n' + 'BOOTPROTO="static"\n' + 'IPADDR="10.0.0.5"\n' + 'NETMASK="255.255.255.0"\n'.format(int=test_int_num)) mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, 'ifup', 'eth' + test_int_num], stderr=-2) @@ -804,17 +1138,36 @@ class TestServerTestCase(base.TestCase): mock_ifaddress.side_effect = [[netifaces.AF_LINK], {netifaces.AF_LINK: [{'addr': '123'}]}] - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - file_name = '/etc/netns/{0}/network/interfaces.d/eth{1}.cfg'.format( - consts.AMPHORA_NAMESPACE, test_int_num) + + if self.conf.conf.amphora_agent.agent_server_network_file: + file_name = self.conf.conf.amphora_agent.agent_server_network_file + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + elif distro == consts.UBUNTU: + file_name = ('/etc/netns/{0}/network/interfaces.d/' + 'eth{1}.cfg'.format(consts.AMPHORA_NAMESPACE, + test_int_num)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + elif distro == consts.CENTOS: + file_name = ('/etc/netns/{0}/sysconfig/network-scripts/' + 'ifcfg-eth{1}'.format(consts.AMPHORA_NAMESPACE, + test_int_num)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.post('/' + api_server.VERSION + "/plug/network", - content_type='application/json', - data=json.dumps(port_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) self.assertEqual(202, rv.status_code) mock_open.assert_any_call(file_name, flags, mode) @@ -827,13 +1180,23 @@ class TestServerTestCase(base.TestCase): mock_fdopen.assert_any_call(123, 'r+') handle = m() - handle.write.assert_any_call( - '\n\n# Generated by Octavia agent\n' - 'auto eth' + test_int_num + - '\niface eth' + test_int_num + ' 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') + if distro == consts.UBUNTU: + handle.write.assert_any_call( + '\n\n# Generated by Octavia agent\n' + 'auto eth{int}\n' + '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)) + elif distro == consts.CENTOS: + handle.write.assert_any_call( + '\n\n# Generated by Octavia agent\n' + 'NM_CONTROLLED="no"\nDEVICE="eth{int}"\n' + 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes"\n' + 'IPV6INIT="yes"\nIPV6_MTU="1450"\n' + 'IPV6_AUTOCONF="no"\n' + 'IPV6ADDR="2001:0db8:0000:0000:0000:0000:' + '0000:0002"\n'.format(int=test_int_num)) mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, 'ifup', 'eth' + test_int_num], stderr=-2) @@ -853,9 +1216,16 @@ class TestServerTestCase(base.TestCase): with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.post('/' + api_server.VERSION + "/plug/network", - content_type='application/json', - data=json.dumps(port_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) self.assertEqual(400, rv.status_code) # same as above but ifup fails @@ -870,23 +1240,69 @@ class TestServerTestCase(base.TestCase): m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): - rv = self.app.post('/' + api_server.VERSION + "/plug/network", - content_type='application/json', - data=json.dumps(port_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) self.assertEqual(500, rv.status_code) self.assertEqual( {'details': RANDOM_ERROR, 'message': 'Error plugging network'}, json.loads(rv.data.decode('utf-8'))) + # Bad port_info tests + port_info = 'Bad data' + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + self.assertEqual(400, rv.status_code) + + port_info = {'fixed_ips': [{'ip_address': '10.0.0.5', + 'subnet_cidr': '10.0.0.0/24'}]} + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + self.assertEqual(400, rv.status_code) + + def test_ubuntu_plug_network_host_routes(self): + self._test_plug_network_host_routes(consts.UBUNTU) + self.conf.config(group="amphora_agent", + agent_server_network_file="/path/to/interfaces_file") + + def test_centos_plug_network_host_routes(self): + self._test_plug_network_host_routes(consts.CENTOS) + @mock.patch('netifaces.interfaces') @mock.patch('netifaces.ifaddresses') @mock.patch('pyroute2.IPRoute') @mock.patch('pyroute2.NetNS') @mock.patch('subprocess.check_output') - def test_plug_network_host_routes(self, mock_check_output, mock_netns, - mock_pyroute2, mock_ifaddress, - mock_interfaces): + def _test_plug_network_host_routes(self, distro, mock_check_output, + mock_netns, mock_pyroute2, + mock_ifaddress, mock_interfaces): + + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) + SUBNET_CIDR = '192.0.2.0/24' BROADCAST = '192.0.2.255' NETMASK = '255.255.255.0' @@ -910,15 +1326,27 @@ class TestServerTestCase(base.TestCase): flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - file_name = '/etc/netns/{0}/network/interfaces.d/{1}.cfg'.format( - consts.AMPHORA_NAMESPACE, consts.NETNS_PRIMARY_INTERFACE) + if distro == consts.UBUNTU: + file_name = '/etc/netns/{0}/network/interfaces.d/{1}.cfg'.format( + consts.AMPHORA_NAMESPACE, consts.NETNS_PRIMARY_INTERFACE) + elif distro == consts.CENTOS: + file_name = ('/etc/netns/{0}/sysconfig/network-scripts/' + 'ifcfg-{1}'.format(consts.AMPHORA_NAMESPACE, + consts.NETNS_PRIMARY_INTERFACE)) m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.post('/' + api_server.VERSION + "/plug/network", - content_type='application/json', - data=json.dumps(port_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) self.assertEqual(202, rv.status_code) mock_open.assert_any_call(file_name, flags, mode) @@ -931,26 +1359,50 @@ class TestServerTestCase(base.TestCase): mock_fdopen.assert_any_call(123, 'r+') handle = m() - handle.write.assert_any_call( - '\n\n# Generated by Octavia agent\n' - 'auto ' + consts.NETNS_PRIMARY_INTERFACE + - '\niface ' + consts.NETNS_PRIMARY_INTERFACE + - ' inet static\n' + - 'address ' + IP + '\nbroadcast ' + BROADCAST + '\n' + - 'netmask ' + NETMASK + '\n' + 'mtu 1450\n' + - 'up route add -net ' + DEST1 + ' gw ' + NEXTHOP + - ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' - 'down route del -net ' + DEST1 + ' gw ' + NEXTHOP + - ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' - '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' - ) + if distro == consts.UBUNTU: + handle.write.assert_any_call( + '\n\n# Generated by Octavia agent\n' + 'auto ' + consts.NETNS_PRIMARY_INTERFACE + + '\niface ' + consts.NETNS_PRIMARY_INTERFACE + + ' inet static\n' + + 'address ' + IP + '\nbroadcast ' + BROADCAST + '\n' + + 'netmask ' + NETMASK + '\n' + 'mtu 1450\n' + + 'up route add -net ' + DEST1 + ' gw ' + NEXTHOP + + ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' + 'down route del -net ' + DEST1 + ' gw ' + NEXTHOP + + ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' + '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' + ) + elif distro == consts.CENTOS: + handle.write.assert_any_call( + '\n\n# Generated by Octavia agent\n' + 'NM_CONTROLLED="no"\nDEVICE="{int}"\n' + 'ONBOOT="yes"\nTYPE="Ethernet"\n' + 'USERCTL="yes"\nIPV6INIT="no"\nMTU="1450"\n' + 'BOOTPROTO="static"\nIPADDR="{ip}"\n' + 'NETMASK="{mask}"\n'.format( + int=consts.NETNS_PRIMARY_INTERFACE, + ip=IP, + mask=NETMASK)) mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, 'ifup', consts.NETNS_PRIMARY_INTERFACE], stderr=-2) + def test_ubuntu_plug_VIP4(self): + self._test_plug_VIP4(consts.UBUNTU) + self.conf.config(group="amphora_agent", + agent_server_network_file="/path/to/interfaces_file") + self._test_plug_VIP4(consts.UBUNTU) + + self._test_plug_VIP4(consts.CENTOS) + self.conf.config(group="amphora_agent", + agent_server_network_file="/path/to/interfaces_file") + self._test_plug_VIP4(consts.CENTOS) + + @mock.patch('shutil.copy2') @mock.patch('pyroute2.NSPopen') @mock.patch('octavia.amphorae.backends.agent.api_server.' 'plug.Plug._netns_interface_exists') @@ -962,11 +1414,12 @@ class TestServerTestCase(base.TestCase): @mock.patch('subprocess.check_output') @mock.patch('shutil.copytree') @mock.patch('os.makedirs') - def test_plug_VIP4(self, mock_makedirs, mock_copytree, mock_check_output, - mock_netns, mock_netns_create, mock_pyroute2, - mock_ifaddress, mock_interfaces, mock_int_exists, - mock_nspopen): + def _test_plug_VIP4(self, distro, mock_makedirs, mock_copytree, + mock_check_output, mock_netns, mock_netns_create, + mock_pyroute2, mock_ifaddress, mock_interfaces, + mock_int_exists, mock_nspopen, mock_copy2): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) subnet_info = { 'subnet_cidr': '203.0.113.0/24', 'gateway': '203.0.113.1', @@ -974,20 +1427,40 @@ class TestServerTestCase(base.TestCase): } # malformed ip - rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error', - data=json.dumps(subnet_info), - content_type='application/json') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + '/plug/vip/error', + data=json.dumps(subnet_info), + content_type='application/json') + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + '/plug/vip/error', + data=json.dumps(subnet_info), + content_type='application/json') self.assertEqual(400, rv.status_code) # No subnet info - rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + '/plug/vip/error') + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + '/plug/vip/error') + self.assertEqual(400, rv.status_code) # Interface already plugged mock_int_exists.return_value = True - rv = self.app.post('/' + api_server.VERSION + "/plug/vip/203.0.113.2", - content_type='application/json', - data=json.dumps(subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) self.assertEqual(409, rv.status_code) self.assertEqual(dict(message="Interface already exists"), json.loads(rv.data.decode('utf-8'))) @@ -995,9 +1468,16 @@ class TestServerTestCase(base.TestCase): # No interface at all mock_interfaces.side_effect = [[]] - rv = self.app.post('/' + api_server.VERSION + "/plug/vip/203.0.113.2", - content_type='application/json', - data=json.dumps(subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) self.assertEqual(404, rv.status_code) self.assertEqual(dict(details="No suitable network interface found"), json.loads(rv.data.decode('utf-8'))) @@ -1005,9 +1485,16 @@ class TestServerTestCase(base.TestCase): # Two interfaces down mock_interfaces.side_effect = [['blah', 'blah2']] mock_ifaddress.side_effect = [['blabla'], ['blabla']] - rv = self.app.post('/' + api_server.VERSION + "/plug/vip/203.0.113.2", - content_type='application/json', - data=json.dumps(subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) self.assertEqual(404, rv.status_code) self.assertEqual(dict(details="No suitable network interface found"), json.loads(rv.data.decode('utf-8'))) @@ -1029,21 +1516,39 @@ class TestServerTestCase(base.TestCase): mock_ifaddress.side_effect = [[netifaces.AF_LINK], {netifaces.AF_LINK: [{'addr': '123'}]}] - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - file_name = ('/etc/netns/{netns}/network/interfaces.d/' - '{netns_int}.cfg'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + + if self.conf.conf.amphora_agent.agent_server_network_file: + file_name = self.conf.conf.amphora_agent.agent_server_network_file + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + elif distro == consts.UBUNTU: + file_name = ('/etc/netns/{netns}/network/interfaces.d/' + '{netns_int}.cfg'.format( + netns=consts.AMPHORA_NAMESPACE, + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + elif distro == consts.CENTOS: + file_name = ('/etc/netns/{netns}/sysconfig/network-scripts/' + 'ifcfg-{netns_int}'.format( + netns=consts.AMPHORA_NAMESPACE, + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.post('/' + api_server.VERSION + - "/plug/vip/203.0.113.2", - content_type='application/json', - data=json.dumps(full_subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(full_subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(full_subnet_info)) self.assertEqual(202, rv.status_code) mock_open.assert_any_call(file_name, flags, mode) mock_fdopen.assert_any_call(123, 'w') @@ -1055,29 +1560,39 @@ class TestServerTestCase(base.TestCase): mock_fdopen.assert_any_call(123, 'r+') handle = m() - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto {netns_int} {netns_int}:0\n' - 'iface {netns_int} inet static\n' - 'address 203.0.113.4\n' - 'broadcast 203.0.113.255\n' - 'netmask 255.255.255.0\n' - 'gateway 203.0.113.1\n' - 'mtu 1450\n' - 'up route add -net 203.0.114.0/24 gw 203.0.113.5 ' - 'dev {netns_int}\n' - 'down route del -net 203.0.114.0/24 gw 203.0.113.5 ' - 'dev {netns_int}\n' - 'up route add -net 203.0.115.0/24 gw 203.0.113.5 ' - 'dev {netns_int}\n' - 'down route del -net 203.0.115.0/24 gw 203.0.113.5 ' - 'dev {netns_int}\n' - '\n' - 'iface {netns_int}:0 inet static\n' - 'address 203.0.113.2\n' - 'broadcast 203.0.113.255\n' - 'netmask 255.255.255.0'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + if distro == consts.UBUNTU: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'auto {netns_int} {netns_int}:0\n' + 'iface {netns_int} inet static\n' + 'address 203.0.113.4\n' + 'broadcast 203.0.113.255\n' + 'netmask 255.255.255.0\n' + 'gateway 203.0.113.1\n' + 'mtu 1450\n' + 'up route add -net 203.0.114.0/24 gw 203.0.113.5 ' + 'dev {netns_int}\n' + 'down route del -net 203.0.114.0/24 gw 203.0.113.5 ' + 'dev {netns_int}\n' + 'up route add -net 203.0.115.0/24 gw 203.0.113.5 ' + 'dev {netns_int}\n' + 'down route del -net 203.0.115.0/24 gw 203.0.113.5 ' + 'dev {netns_int}\n' + '\n' + 'iface {netns_int}:0 inet static\n' + 'address 203.0.113.2\n' + 'broadcast 203.0.113.255\n' + 'netmask 255.255.255.0'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + elif distro == consts.CENTOS: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'NM_CONTROLLED="no"\nDEVICE="{netns_int}"\n' + 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes" \n' + 'BOOTPROTO="static"\nIPADDR="203.0.113.4"\n' + 'NETMASK="255.255.255.0"\nGATEWAY="203.0.113.1"\n' + 'MTU="1450" \n'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, 'ifup', '{netns_int}:0'.format( @@ -1093,21 +1608,40 @@ class TestServerTestCase(base.TestCase): mock_ifaddress.side_effect = [[netifaces.AF_LINK], {netifaces.AF_LINK: [{'addr': '123'}]}] - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - file_name = ('/etc/netns/{netns}/network/interfaces.d/' - '{netns_int}.cfg'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + + if self.conf.conf.amphora_agent.agent_server_network_file: + file_name = self.conf.conf.amphora_agent.agent_server_network_file + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + + elif distro == consts.UBUNTU: + file_name = ('/etc/netns/{netns}/network/interfaces.d/' + '{netns_int}.cfg'.format( + netns=consts.AMPHORA_NAMESPACE, + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + elif distro == consts.CENTOS: + file_name = ('/etc/netns/{netns}/sysconfig/network-scripts/' + 'ifcfg-{netns_int}'.format( + netns=consts.AMPHORA_NAMESPACE, + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.post('/' + api_server.VERSION + - "/plug/vip/203.0.113.2", - content_type='application/json', - data=json.dumps(subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) self.assertEqual(202, rv.status_code) mock_open.assert_any_call(file_name, flags, mode) mock_fdopen.assert_any_call(123, 'w') @@ -1119,15 +1653,23 @@ class TestServerTestCase(base.TestCase): mock_fdopen.assert_any_call(123, 'r+') handle = m() - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto {netns_int} {netns_int}:0\n' - 'iface {netns_int} inet dhcp\n\n' - 'iface {netns_int}:0 inet static\n' - 'address 203.0.113.2\n' - 'broadcast 203.0.113.255\n' - 'netmask 255.255.255.0'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + if distro == consts.UBUNTU: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'auto {netns_int} {netns_int}:0\n\n' + 'iface {netns_int} inet dhcp\n\n' + 'iface {netns_int}:0 inet static\n' + 'address 203.0.113.2\n' + 'broadcast 203.0.113.255\n' + 'netmask 255.255.255.0'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + elif distro == consts.CENTOS: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'NM_CONTROLLED="no"\nDEVICE="{netns_int}"\n' + 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes" \n' + 'BOOTPROTO="dhcp"\nPERSISTENT_DHCLIENT="1" \n'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, 'ifup', '{netns_int}:0'.format( @@ -1144,16 +1686,29 @@ class TestServerTestCase(base.TestCase): m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): - rv = self.app.post('/' + api_server.VERSION + - "/plug/vip/203.0.113.2", - content_type='application/json', - data=json.dumps(subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/203.0.113.2", + content_type='application/json', + data=json.dumps(subnet_info)) self.assertEqual(500, rv.status_code) self.assertEqual( {'details': RANDOM_ERROR, 'message': 'Error plugging VIP'}, json.loads(rv.data.decode('utf-8'))) + def test_ubuntu_plug_VIP6(self): + self._test_plug_vip6(consts.UBUNTU) + + def test_centos_plug_VIP6(self): + self._test_plug_vip6(consts.CENTOS) + + @mock.patch('shutil.copy2') @mock.patch('pyroute2.NSPopen') @mock.patch('netifaces.interfaces') @mock.patch('netifaces.ifaddresses') @@ -1163,10 +1718,12 @@ class TestServerTestCase(base.TestCase): @mock.patch('subprocess.check_output') @mock.patch('shutil.copytree') @mock.patch('os.makedirs') - def test_plug_vip6(self, mock_makedirs, mock_copytree, mock_check_output, - mock_netns, mock_netns_create, mock_pyroute2, - mock_ifaddress, mock_interfaces, mock_nspopen): + def _test_plug_vip6(self, distro, mock_makedirs, mock_copytree, + mock_check_output, mock_netns, mock_netns_create, + mock_pyroute2, mock_ifaddress, mock_interfaces, + mock_nspopen, mock_copy2): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) subnet_info = { 'subnet_cidr': '2001:db8::/32', 'gateway': '2001:db8::1', @@ -1174,20 +1731,43 @@ class TestServerTestCase(base.TestCase): } # malformed ip - rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error', - data=json.dumps(subnet_info), - content_type='application/json') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + '/plug/vip/error', + data=json.dumps(subnet_info), + content_type='application/json') + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + '/plug/vip/error', + data=json.dumps(subnet_info), + content_type='application/json') self.assertEqual(400, rv.status_code) # No subnet info - rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + '/plug/vip/error', + data=json.dumps(subnet_info), + content_type='application/json') + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + '/plug/vip/error', + data=json.dumps(subnet_info), + content_type='application/json') self.assertEqual(400, rv.status_code) # No interface at all mock_interfaces.side_effect = [[]] - rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2", - content_type='application/json', - data=json.dumps(subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) self.assertEqual(404, rv.status_code) self.assertEqual(dict(details="No suitable network interface found"), json.loads(rv.data.decode('utf-8'))) @@ -1195,9 +1775,16 @@ class TestServerTestCase(base.TestCase): # Two interfaces down mock_interfaces.side_effect = [['blah', 'blah2']] mock_ifaddress.side_effect = [['blabla'], ['blabla']] - rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2", - content_type='application/json', - data=json.dumps(subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) self.assertEqual(404, rv.status_code) self.assertEqual(dict(details="No suitable network interface found"), json.loads(rv.data.decode('utf-8'))) @@ -1221,19 +1808,32 @@ class TestServerTestCase(base.TestCase): flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - file_name = ('/etc/netns/{netns}/network/interfaces.d/' - '{netns_int}.cfg'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + + if distro == consts.UBUNTU: + file_name = ('/etc/netns/{netns}/network/interfaces.d/' + '{netns_int}.cfg'.format( + netns=consts.AMPHORA_NAMESPACE, + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + elif distro == consts.CENTOS: + file_name = ('/etc/netns/{netns}/sysconfig/network-scripts/' + 'ifcfg-{netns_int}'.format( + netns=consts.AMPHORA_NAMESPACE, + netns_int=consts.NETNS_PRIMARY_INTERFACE)) m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.post('/' + api_server.VERSION + - "/plug/vip/2001:db8::2", - content_type='application/json', - data=json.dumps(full_subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(full_subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(full_subnet_info)) self.assertEqual(202, rv.status_code) mock_open.assert_any_call(file_name, flags, mode) mock_fdopen.assert_any_call(123, 'w') @@ -1244,32 +1844,52 @@ class TestServerTestCase(base.TestCase): mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') handle = m() - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto {netns_int} {netns_int}:0\n' - 'iface {netns_int} inet6 static\n' - 'address 2001:db8::4\n' - 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' - 'netmask 32\n' - 'gateway 2001:db8::1\n' - 'mtu 1450\n' - 'up route add -net 2001:db9::/32 gw 2001:db8::5 ' - 'dev {netns_int}\n' - 'down route del -net 2001:db9::/32 gw 2001:db8::5 ' - 'dev {netns_int}\n' - 'up route add -net 2001:db9::/32 gw 2001:db8::5 ' - 'dev {netns_int}\n' - 'down route del -net 2001:db9::/32 gw 2001:db8::5 ' - 'dev {netns_int}\n' - '\n' - 'iface {netns_int}:0 inet6 static\n' - 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' - 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' - 'netmask 32'.format(netns_int=consts.NETNS_PRIMARY_INTERFACE)) - 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) + if distro == consts.UBUNTU: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'auto {netns_int} {netns_int}:0\n' + 'iface {netns_int} inet6 static\n' + 'address 2001:db8::4\n' + 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' + 'netmask 32\n' + 'gateway 2001:db8::1\n' + 'mtu 1450\n' + 'up route add -net 2001:db9::/32 gw 2001:db8::5 ' + 'dev {netns_int}\n' + 'down route del -net 2001:db9::/32 gw 2001:db8::5 ' + 'dev {netns_int}\n' + 'up route add -net 2001:db9::/32 gw 2001:db8::5 ' + 'dev {netns_int}\n' + 'down route del -net 2001:db9::/32 gw 2001:db8::5 ' + 'dev {netns_int}\n' + '\n' + 'iface {netns_int}:0 inet6 static\n' + 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' + 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' + 'netmask 32'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + elif distro == consts.CENTOS: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'NM_CONTROLLED="no"\nDEVICE="{netns_int}"\n' + 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes"\n' + 'IPV6INIT="yes"\nIPV6_DEFROUTE="yes"\n' + 'IPV6_AUTOCONF="no"\nIPV6ADDR="2001:db8::4/32"\n' + 'IPV6_DEFAULTGW="2001:db8::1"\nIPV6_MTU="1450" \n' + 'IPV6ADDR_SECONDARIES="2001:0db8:0000:0000:0000:0000:' + '0000:0002/32"\n'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + + if distro == consts.UBUNTU: + 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) + elif distro == consts.CENTOS: + mock_check_output.assert_called_with( + ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, + 'ifup', '{netns_int}'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) # Verify sysctl was loaded mock_nspopen.assert_called_once_with( @@ -1283,19 +1903,31 @@ class TestServerTestCase(base.TestCase): flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - file_name = ('/etc/netns/{netns}/network/interfaces.d/' - '{netns_int}.cfg'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + if distro == consts.UBUNTU: + file_name = ('/etc/netns/{netns}/network/interfaces.d/' + '{netns_int}.cfg'.format( + netns=consts.AMPHORA_NAMESPACE, + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + elif distro == consts.CENTOS: + file_name = ('/etc/netns/{netns}/sysconfig/network-scripts/' + 'ifcfg-{netns_int}'.format( + netns=consts.AMPHORA_NAMESPACE, + netns_int=consts.NETNS_PRIMARY_INTERFACE)) m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.post('/' + api_server.VERSION + - "/plug/vip/2001:db8::2", - content_type='application/json', - data=json.dumps(subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) self.assertEqual(202, rv.status_code) mock_open.assert_any_call(file_name, flags, mode) mock_fdopen.assert_any_call(123, 'w') @@ -1306,18 +1938,37 @@ class TestServerTestCase(base.TestCase): mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') handle = m() - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto {netns_int} {netns_int}:0\n' - 'iface {netns_int} inet6 auto\n\n' - 'iface {netns_int}:0 inet6 static\n' - 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' - 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' - 'netmask 32'.format(netns_int=consts.NETNS_PRIMARY_INTERFACE)) - 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) + if distro == consts.UBUNTU: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'auto {netns_int} {netns_int}:0\n\n' + 'iface {netns_int} inet6 auto\n\n' + 'iface {netns_int}:0 inet6 static\n' + 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' + 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' + 'netmask 32'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + elif distro == consts.CENTOS: + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'NM_CONTROLLED="no"\nDEVICE="{netns_int}"\n' + 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes" \n' + 'IPV6INIT="yes"\nIPV6_DEFROUTE="yes"\n' + 'IPV6_AUTOCONF="yes" \n' + 'IPV6ADDR_SECONDARIES="2001:0db8:0000:0000:0000:0000:' + '0000:0002/32"\n'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) + + if distro == consts.UBUNTU: + 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) + elif distro == consts.CENTOS: + mock_check_output.assert_called_with( + ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, + 'ifup', '{netns_int}'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) mock_interfaces.side_effect = [['blah']] mock_ifaddress.side_effect = [[netifaces.AF_LINK], @@ -1330,18 +1981,32 @@ class TestServerTestCase(base.TestCase): m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): - rv = self.app.post('/' + api_server.VERSION + - "/plug/vip/2001:db8::2", - content_type='application/json', - data=json.dumps(subnet_info)) + if distro == consts.UBUNTU: + rv = self.ubuntu_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + elif distro == consts.CENTOS: + rv = self.centos_app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) self.assertEqual(500, rv.status_code) self.assertEqual( {'details': RANDOM_ERROR, 'message': 'Error plugging VIP'}, json.loads(rv.data.decode('utf-8'))) + def test_ubuntu_get_interface(self): + self._test_get_interface(consts.UBUNTU) + + def test_centos_get_interface(self): + self._test_get_interface(consts.CENTOS) + @mock.patch('pyroute2.NetNS') - def test_get_interface(self, mock_netns): + def _test_get_interface(self, distro, mock_netns): + + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) netns_handle = mock_netns.return_value.__enter__.return_value @@ -1353,9 +2018,16 @@ class TestServerTestCase(base.TestCase): 'attrs': [['IFA_ADDRESS', '203.0.113.2']]}] netns_handle.get_links.return_value = [{ 'attrs': [['IFLA_IFNAME', 'eth0']]}] - rv = self.app.get('/' + api_server.VERSION + '/interface/203.0.113.2', - data=json.dumps(interface_res), - content_type='application/json') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/interface/203.0.113.2', + data=json.dumps(interface_res), + content_type='application/json') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/interface/203.0.113.2', + data=json.dumps(interface_res), + content_type='application/json') self.assertEqual(200, rv.status_code) # Happy path with IPv6 address normalization @@ -1365,77 +2037,79 @@ class TestServerTestCase(base.TestCase): '0000:0000:0000:0000:0000:0000:0000:0001']]}] netns_handle.get_links.return_value = [{ 'attrs': [['IFLA_IFNAME', 'eth0']]}] - rv = self.app.get('/' + api_server.VERSION + '/interface/::1', - data=json.dumps(interface_res), - content_type='application/json') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/interface/::1', + data=json.dumps(interface_res), + content_type='application/json') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/interface/::1', + data=json.dumps(interface_res), + content_type='application/json') self.assertEqual(200, rv.status_code) # Nonexistent interface - rv = self.app.get('/' + api_server.VERSION + '/interface/10.0.0.1', - data=json.dumps(interface_res), - content_type='application/json') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/interface/10.0.0.1', + data=json.dumps(interface_res), + content_type='application/json') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/interface/10.0.0.1', + data=json.dumps(interface_res), + content_type='application/json') self.assertEqual(404, rv.status_code) # Invalid IP address - rv = self.app.get('/' + api_server.VERSION + - '/interface/00:00:00:00:00:00', - data=json.dumps(interface_res), - content_type='application/json') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + + '/interface/00:00:00:00:00:00', + data=json.dumps(interface_res), + content_type='application/json') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + + '/interface/00:00:00:00:00:00', + data=json.dumps(interface_res), + content_type='application/json') self.assertEqual(400, rv.status_code) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSTEMD) - @mock.patch('os.path.exists') - @mock.patch('os.makedirs') - @mock.patch('os.rename') - @mock.patch('subprocess.check_output') - @mock.patch('os.remove') - def test_upload_keepalived_config_systemd(self, mock_remove, - mock_subprocess, mock_rename, - mock_makedirs, mock_exists, - mock_init_system): - self._test_upload_keepalived_config(mock_remove, mock_subprocess, - mock_rename, mock_makedirs, - mock_exists, mock_init_system, - consts.INIT_SYSTEMD) + def test_ubuntu_upload_keepalived_config_systemd(self, mock_init_system): + self._test_upload_keepalived_config(consts.INIT_SYSTEMD, + consts.UBUNTU, mock_init_system) + + @mock.patch('octavia.amphorae.backends.agent.api_server.util.' + 'get_os_init_system', return_value=consts.INIT_SYSTEMD) + def test_centos_upload_keepalived_config_systemd(self, mock_init_system): + self._test_upload_keepalived_config(consts.INIT_SYSTEMD, + consts.CENTOS, mock_init_system) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_UPSTART) - @mock.patch('os.path.exists') - @mock.patch('os.makedirs') - @mock.patch('os.rename') - @mock.patch('subprocess.check_output') - @mock.patch('os.remove') - def test_upload_keepalived_config_upstart(self, mock_remove, - mock_subprocess, mock_rename, - mock_makedirs, mock_exists, - mock_init_system): - self._test_upload_keepalived_config(mock_remove, mock_subprocess, - mock_rename, mock_makedirs, - mock_exists, mock_init_system, - consts.INIT_UPSTART) + def test_ubuntu_upload_keepalived_config_upstart(self, mock_init_system): + self._test_upload_keepalived_config(consts.INIT_UPSTART, + consts.UBUNTU, mock_init_system) @mock.patch('octavia.amphorae.backends.agent.api_server.util.' 'get_os_init_system', return_value=consts.INIT_SYSVINIT) + def test_ubuntu_upload_keepalived_config_sysvinit(self, mock_init_system): + self._test_upload_keepalived_config(consts.INIT_SYSVINIT, + consts.UBUNTU, mock_init_system) + @mock.patch('os.path.exists') @mock.patch('os.makedirs') @mock.patch('os.rename') @mock.patch('subprocess.check_output') @mock.patch('os.remove') - def test_upload_keepalived_config_sysvinit(self, mock_remove, - mock_subprocess, mock_rename, - mock_makedirs, mock_exists, - mock_init_system): - self._test_upload_keepalived_config(mock_remove, mock_subprocess, - mock_rename, mock_makedirs, - mock_exists, mock_init_system, - consts.INIT_SYSVINIT) - - def _test_upload_keepalived_config(self, mock_remove, mock_subprocess, - mock_rename, mock_makedirs, - mock_exists, mock_init_system, - init_system): + def _test_upload_keepalived_config(self, init_system, distro, + mock_init_system, mock_remove, + mock_subprocess, mock_rename, + mock_makedirs, mock_exists): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mock_exists.return_value = True @@ -1445,8 +2119,12 @@ class TestServerTestCase(base.TestCase): with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload', - data='test') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/vrrp/upload', data='test') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/vrrp/upload', data='test') mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH mock_open.assert_called_with(cfg_path, flags, mode) @@ -1460,24 +2138,204 @@ class TestServerTestCase(base.TestCase): with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: mock_open.return_value = 123 - rv = self.app.put('/' + api_server.VERSION + '/vrrp/upload', - data='test') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/vrrp/upload', data='test') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/vrrp/upload', data='test') mode = (stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) mock_open.assert_called_with(script_path, flags, mode) mock_fdopen.assert_called_with(123, 'w') self.assertEqual(200, rv.status_code) + def test_ubuntu_manage_service_vrrp(self): + self._test_manage_service_vrrp(consts.UBUNTU) + + def test_centos_manage_service_vrrp(self): + self._test_manage_service_vrrp(consts.CENTOS) + @mock.patch('subprocess.check_output') - def test_manage_service_vrrp(self, mock_check_output): - rv = self.app.put('/' + api_server.VERSION + '/vrrp/start') + def _test_manage_service_vrrp(self, distro, mock_check_output): + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) + + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + '/vrrp/start') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + '/vrrp/start') + self.assertEqual(202, rv.status_code) - rv = self.app.put('/' + api_server.VERSION + '/vrrp/restart') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + + '/vrrp/restart') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + + '/vrrp/restart') self.assertEqual(400, rv.status_code) mock_check_output.side_effect = subprocess.CalledProcessError(1, 'blah!') - rv = self.app.put('/' + api_server.VERSION + '/vrrp/start') + if distro == consts.UBUNTU: + rv = self.ubuntu_app.put('/' + api_server.VERSION + '/vrrp/start') + elif distro == consts.CENTOS: + rv = self.centos_app.put('/' + api_server.VERSION + '/vrrp/start') self.assertEqual(500, rv.status_code) + + def test_ubuntu_details(self): + self._test_details(consts.UBUNTU) + + def test_centos_details(self): + self._test_details(consts.CENTOS) + + @mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.' + 'AmphoraInfo._count_haproxy_processes') + @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('os.statvfs') + @mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.' + 'AmphoraInfo._cpu') + @mock.patch('octavia.amphorae.backends.agent.api_server.amphora_info.' + 'AmphoraInfo._get_meminfo') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'util.get_listeners') + @mock.patch('socket.gethostname') + @mock.patch('subprocess.check_output') + 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): + + self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) + + listener_id = uuidutils.generate_uuid() + mock_get_listeners.return_value = [listener_id] + + mock_hostname.side_effect = ['test-host'] + + mock_subbprocess.side_effect = [ + """Package: haproxy + Status: install ok installed + Priority: optional + Section: net + Installed-Size: 803 + Maintainer: Ubuntu Developers + Architecture: amd64 + Version: 9.9.99-9 + """] + + MemTotal = random.randrange(0, 1000) + MemFree = random.randrange(0, 1000) + Buffers = random.randrange(0, 1000) + Cached = random.randrange(0, 1000) + SwapCached = random.randrange(0, 1000) + Shmem = random.randrange(0, 1000) + Slab = random.randrange(0, 1000) + + memory_dict = {'CmaFree': 0, 'Mapped': 38244, 'CommitLimit': 508048, + 'MemFree': MemFree, 'AnonPages': 92384, + 'DirectMap2M': 997376, 'SwapTotal': 0, + 'NFS_Unstable': 0, 'SReclaimable': 34168, + 'Writeback': 0, 'PageTables': 3760, 'Shmem': Shmem, + 'Hugepagesize': 2048, 'MemAvailable': 738356, + 'HardwareCorrupted': 0, 'SwapCached': SwapCached, + 'Dirty': 80, 'Active': 237060, 'VmallocUsed': 0, + 'Inactive(anon)': 2752, 'Slab': Slab, 'Cached': Cached, + 'Inactive(file)': 149588, 'SUnreclaim': 17796, + 'Mlocked': 3656, 'AnonHugePages': 6144, 'SwapFree': 0, + 'Active(file)': 145512, 'CmaTotal': 0, + 'Unevictable': 3656, 'KernelStack': 2368, + 'Inactive': 152340, 'MemTotal': MemTotal, 'Bounce': 0, + 'Committed_AS': 401884, 'Active(anon)': 91548, + 'VmallocTotal': 34359738367, 'VmallocChunk': 0, + 'DirectMap4k': 51072, 'WritebackTmp': 0, + 'Buffers': Buffers} + mock_get_mem.return_value = memory_dict + + cpu_total = random.randrange(0, 1000) + cpu_user = random.randrange(0, 1000) + cpu_system = random.randrange(0, 1000) + cpu_softirq = random.randrange(0, 1000) + + cpu_dict = {'idle': '7168848', 'system': cpu_system, + 'total': cpu_total, 'softirq': cpu_softirq, 'nice': '31', + 'iowait': '902', 'user': cpu_user, 'irq': '0'} + + mock_cpu.return_value = cpu_dict + + f_blocks = random.randrange(0, 1000) + f_bfree = random.randrange(0, 1000) + f_frsize = random.randrange(0, 1000) + f_bavail = random.randrange(0, 1000) + + stats = mock.MagicMock() + stats.f_blocks = f_blocks + stats.f_bfree = f_bfree + stats.f_frsize = f_frsize + stats.f_bavail = f_bavail + disk_used = (f_blocks - f_bfree) * f_frsize + disk_available = f_bavail * f_frsize + + mock_statvfs.return_value = stats + + load_1min = random.randrange(0, 10) + load_5min = random.randrange(0, 10) + load_15min = random.randrange(0, 10) + + mock_load.return_value = [load_1min, load_5min, load_15min] + + eth1_rx = random.randrange(0, 1000) + eth1_tx = random.randrange(0, 1000) + eth2_rx = random.randrange(0, 1000) + eth2_tx = random.randrange(0, 1000) + eth3_rx = random.randrange(0, 1000) + eth3_tx = random.randrange(0, 1000) + + net_dict = {'eth2': {'network_rx': eth2_rx, 'network_tx': eth2_tx}, + 'eth1': {'network_rx': eth1_rx, 'network_tx': eth1_tx}, + 'eth3': {'network_rx': eth3_rx, 'network_tx': eth3_tx}} + + mock_get_nets.return_value = net_dict + + haproxy_count = random.randrange(0, 100) + mock_count_haproxy.return_value = haproxy_count + + expected_dict = {'active': True, 'api_version': '0.5', + 'cpu': {'soft_irq': cpu_softirq, 'system': cpu_system, + 'total': cpu_total, 'user': cpu_user}, + 'disk': {'available': disk_available, + 'used': disk_used}, + 'haproxy_count': haproxy_count, + 'haproxy_version': '9.9.99-9', + 'hostname': 'test-host', + 'listeners': [listener_id], + 'load': [load_1min, load_5min, load_15min], + 'memory': {'buffers': Buffers, + 'cached': Cached, + 'free': MemFree, + 'shared': Shmem, + 'slab': Slab, + 'swap_used': SwapCached, + 'total': MemTotal}, + 'networks': {'eth1': {'network_rx': eth1_rx, + 'network_tx': eth1_tx}, + 'eth2': {'network_rx': eth2_rx, + 'network_tx': eth2_tx}, + 'eth3': {'network_rx': eth3_rx, + 'network_tx': eth3_tx}}, + 'packages': {}, + 'topology': consts.TOPOLOGY_SINGLE, + 'topology_status': consts.TOPOLOGY_STATUS_OK} + + if distro == consts.UBUNTU: + rv = self.ubuntu_app.get('/' + api_server.VERSION + '/details') + elif distro == consts.CENTOS: + rv = self.centos_app.get('/' + api_server.VERSION + '/details') + + self.assertEqual(200, rv.status_code) + self.assertEqual(expected_dict, + json.loads(rv.data.decode('utf-8'))) 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 new file mode 100644 index 0000000000..1ea7351910 --- /dev/null +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_amphora_info.py @@ -0,0 +1,206 @@ +# Copyright 2017 Rackspace US, Inc. +# +# 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 random + +import mock +from oslo_utils import uuidutils + +from octavia.amphorae.backends.agent import api_server +from octavia.amphorae.backends.agent.api_server import amphora_info +from octavia.tests.common import utils as test_utils +import octavia.tests.unit.base as base + + +class TestAmphoraInfo(base.TestCase): + + API_VERSION = random.randrange(0, 10000) + HAPROXY_VERSION = random.randrange(0, 10000) + + def setUp(self): + super(TestAmphoraInfo, self).setUp() + self.osutils_mock = mock.MagicMock() + self.amp_info = amphora_info.AmphoraInfo(self.osutils_mock) + + @mock.patch.object(amphora_info, "flask") + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'amphora_info.AmphoraInfo._get_version_of_installed_package', + return_value=HAPROXY_VERSION) + @mock.patch('socket.gethostname', return_value='FAKE_HOST') + def test_compile_amphora_info(self, mock_gethostname, mock_pkg_version, + mock_flask): + 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} + self.amp_info.compile_amphora_info() + mock_flask.jsonify.assert_called_once_with(expected_dict) + 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): + + # Test no listeners passed in + result = self.amp_info._count_haproxy_processes([]) + self.assertEqual(0, result) + + # Test with a listener specified + mock_is_running.side_effect = [True, False] + result = self.amp_info._count_haproxy_processes( + [uuidutils.generate_uuid(), uuidutils.generate_uuid()]) + self.assertEqual(1, result) + + def test__get_meminfo(self): + + # Test live data from host + + result = self.amp_info._get_meminfo() + + # check that /proc/meminfo is giving us the fields we use + self.assertIn('MemTotal', result) + self.assertIn('MemFree', result) + self.assertIn('Buffers', result) + self.assertIn('Cached', result) + self.assertIn('SwapCached', result) + self.assertIn('Shmem', result) + self.assertIn('Slab', result) + + # Known data test + meminfo = ('MemTotal: 21692784 kB\n' + 'MemFree: 12685624 kB\n' + 'MemAvailable: 17384072 kB\n' + 'Buffers: 344792 kB\n' + 'Cached: 4271856 kB\n' + 'SwapCached: 0 kB\n' + 'Active: 5808816 kB\n' + 'Inactive: 2445236 kB\n' + 'Active(anon): 3646184 kB\n' + 'Inactive(anon): 8672 kB\n' + 'Active(file): 2162632 kB\n' + 'Inactive(file): 2436564 kB\n' + 'Unevictable: 52664 kB\n' + 'Mlocked: 52664 kB\n' + 'SwapTotal: 20476924 kB\n' + 'SwapFree: 20476924 kB\n' + 'Dirty: 92 kB\n' + 'Writeback: 0 kB\n' + 'AnonPages: 3690088 kB\n' + 'Mapped: 108520 kB\n' + 'Shmem: 9520 kB\n' + 'Slab: 534384 kB\n' + 'SReclaimable: 458160 kB\n' + 'SUnreclaim: 76224 kB\n' + 'KernelStack: 11776 kB\n' + 'PageTables: 33088 kB\n' + 'NFS_Unstable: 0 kB\n' + 'Bounce: 0 kB\n' + 'WritebackTmp: 0 kB\n' + 'CommitLimit: 31323316 kB\n' + 'Committed_AS: 6930732 kB\n' + 'VmallocTotal: 34359738367 kB\n' + 'VmallocUsed: 0 kB\n' + 'VmallocChunk: 0 kB\n' + 'HardwareCorrupted: 0 kB\n' + 'AnonHugePages: 1400832 kB\n' + 'CmaTotal: 0 kB\n' + 'CmaFree: 0 kB\n' + 'HugePages_Total: 0\n' + 'HugePages_Free: 0\n' + 'HugePages_Rsvd: 0\n' + 'HugePages_Surp: 0\n' + 'Hugepagesize: 2048 kB\n' + 'DirectMap4k: 130880 kB\n' + 'DirectMap2M: 8376320 kB\n' + 'DirectMap1G: 14680064 kB\n') + + self.useFixture(test_utils.OpenFixture('/proc/meminfo', + contents=meminfo)) + + expected_result = {'SwapCached': 0, 'DirectMap2M': 8376320, + 'CmaTotal': 0, 'Inactive': 2445236, + 'KernelStack': 11776, 'SwapTotal': 20476924, + 'VmallocUsed': 0, 'Buffers': 344792, + 'MemTotal': 21692784, 'Mlocked': 52664, + 'Cached': 4271856, 'AnonPages': 3690088, + 'Unevictable': 52664, 'SUnreclaim': 76224, + 'MemFree': 12685624, 'Writeback': 0, + 'NFS_Unstable': 0, 'VmallocTotal': 34359738367, + 'MemAvailable': 17384072, 'CmaFree': 0, + 'SwapFree': 20476924, 'AnonHugePages': 1400832, + 'DirectMap1G': 14680064, 'Hugepagesize': 2048, + 'Dirty': 92, 'Bounce': 0, 'PageTables': 33088, + 'SReclaimable': 458160, 'Active': 5808816, + 'Mapped': 108520, 'Slab': 534384, + 'Active(anon)': 3646184, 'VmallocChunk': 0, + 'Inactive(file)': 2436564, 'WritebackTmp': 0, + 'Shmem': 9520, 'Inactive(anon)': 8672, + 'HardwareCorrupted': 0, 'Active(file)': 2162632, + 'DirectMap4k': 130880, 'Committed_AS': 6930732, + 'CommitLimit': 31323316} + + result = self.amp_info._get_meminfo() + self.assertEqual(expected_result, result) + + def test__cpu(self): + + sample_stat = 'cpu 252551 802 52554 7181757 7411 0 8336 0 0 0' + + expected_result = {'user': '252551', 'iowait': '7411', 'nice': '802', + 'softirq': '8336', 'idle': '7181757', + 'system': '52554', 'total': 7503411, 'irq': '0'} + + self.useFixture(test_utils.OpenFixture('/proc/stat', + contents=sample_stat)) + + result = self.amp_info._cpu() + + self.assertEqual(expected_result, result) + + def test__load(self): + + sample_loadavg = '0.09 0.11 0.10 2/630 15346' + + expected_result = ['0.09', '0.11', '0.10'] + + self.useFixture(test_utils.OpenFixture('/proc/loadavg', + contents=sample_loadavg)) + + result = self.amp_info._load() + + self.assertEqual(expected_result, result) + + @mock.patch('pyroute2.NetNS') + def test__get_networks(self, mock_netns): + + # The output of get_links is huge, just pulling out the parts we + # care about for this test. + sample_get_links_minimal = [ + {'attrs': [('IFLA_IFNAME', 'lo')]}, + {'attrs': [('IFLA_IFNAME', 'eth1'), + ('IFLA_STATS64', {'tx_bytes': 418, 'rx_bytes': 996})]}, + {'attrs': [('IFLA_IFNAME', 'eth2'), + ('IFLA_STATS64', {'tx_bytes': 578, 'rx_bytes': 848})]}, + {'attrs': [('IFLA_IFNAME', 'eth3')]}] + + netns_handle = mock_netns.return_value.__enter__.return_value + netns_handle.get_links.return_value = sample_get_links_minimal + + expected_result = {'eth1': {'network_rx': 996, 'network_tx': 418}, + 'eth2': {'network_rx': 848, 'network_tx': 578}} + + result = self.amp_info._get_networks() + + self.assertEqual(expected_result, result) diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py new file mode 100644 index 0000000000..762c695e4b --- /dev/null +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py @@ -0,0 +1,217 @@ +# Copyright 2017 Redhat. +# +# 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 shutil + +import mock +from oslo_config import fixture as oslo_fixture + +from octavia.amphorae.backends.agent.api_server import osutils +from octavia.common import config +from octavia.common import constants as consts +from octavia.common import exceptions as octavia_exceptions +from octavia.tests.unit import base + + +class TestOSUtils(base.TestCase): + + def setUp(self): + super(TestOSUtils, self).setUp() + + self.base_os_util = osutils.BaseOS('unknown') + + with mock.patch('platform.linux_distribution', + return_value=['Ubuntu', 'Foo', 'Bar']): + self.ubuntu_os_util = osutils.BaseOS.get_os_util() + + with mock.patch('platform.linux_distribution', + return_value=['centos', 'Foo', 'Bar']): + self.rh_os_util = osutils.BaseOS.get_os_util() + + def test_get_os_util(self): + with mock.patch('platform.linux_distribution', + return_value=['Ubuntu', 'Foo', 'Bar']): + returned_cls = osutils.BaseOS.get_os_util() + self.assertIsInstance(returned_cls, osutils.Ubuntu) + with mock.patch('platform.linux_distribution', + return_value=['fedora', 'Foo', 'Bar']): + returned_cls = osutils.BaseOS.get_os_util() + self.assertIsInstance(returned_cls, osutils.RH) + with mock.patch('platform.linux_distribution', + return_value=['redhat', 'Foo', 'Bar']): + returned_cls = osutils.BaseOS.get_os_util() + self.assertIsInstance(returned_cls, osutils.RH) + with mock.patch('platform.linux_distribution', + return_value=['centos', 'Foo', 'Bar']): + returned_cls = osutils.BaseOS.get_os_util() + self.assertIsInstance(returned_cls, osutils.RH) + with mock.patch('platform.linux_distribution', + return_value=['FakeOS', 'Foo', 'Bar']): + self.assertRaises( + octavia_exceptions.InvalidAmphoraOperatingSystem, + osutils.BaseOS.get_os_util) + + def test_get_network_interface_file(self): + conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF)) + + fake_agent_server_network_dir = "/path/to/interface" + fake_agent_server_network_file = "/path/to/interfaces_file" + + base_fake_nic_path = os.path.join(fake_agent_server_network_dir, + consts.NETNS_PRIMARY_INTERFACE) + base_real_nic_path = os.path.join( + consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format( + netns=consts.AMPHORA_NAMESPACE), + consts.NETNS_PRIMARY_INTERFACE) + + rh_interface_name = 'ifcfg-{nic}'.format( + nic=consts.NETNS_PRIMARY_INTERFACE) + rh_fake_nic_path = os.path.join(fake_agent_server_network_dir, + rh_interface_name) + rh_real_nic_path = os.path.join( + consts.RH_AMP_NET_DIR_TEMPLATE.format( + netns=consts.AMPHORA_NAMESPACE), + rh_interface_name) + + ubuntu_interface_name = '{nic}.cfg'.format( + nic=consts.NETNS_PRIMARY_INTERFACE) + ubuntu_fake_nic_path = os.path.join(fake_agent_server_network_dir, + ubuntu_interface_name) + ubuntu_real_nic_path = os.path.join( + consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format( + netns=consts.AMPHORA_NAMESPACE), + ubuntu_interface_name) + + # Check that agent_server_network_file is returned, when provided + conf.config(group="amphora_agent", + agent_server_network_file=fake_agent_server_network_file) + + base_interface_file = ( + self.base_os_util. + get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) + self.assertEqual(fake_agent_server_network_file, base_interface_file) + + rh_interface_file = ( + self.rh_os_util. + get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) + self.assertEqual(fake_agent_server_network_file, rh_interface_file) + + ubuntu_interface_file = ( + self.ubuntu_os_util. + get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) + self.assertEqual(fake_agent_server_network_file, ubuntu_interface_file) + + # Check that agent_server_network_dir is used, when provided + conf.config(group="amphora_agent", agent_server_network_file=None) + conf.config(group="amphora_agent", + agent_server_network_dir=fake_agent_server_network_dir) + + base_interface_file = ( + self.base_os_util. + get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) + self.assertEqual(base_fake_nic_path, base_interface_file) + + rh_interface_file = ( + self.rh_os_util. + get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) + self.assertEqual(rh_fake_nic_path, rh_interface_file) + + ubuntu_interface_file = ( + self.ubuntu_os_util. + get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) + self.assertEqual(ubuntu_fake_nic_path, ubuntu_interface_file) + + # Check When neither agent_server_network_dir or + # agent_server_network_file where provided. + conf.config(group="amphora_agent", agent_server_network_file=None) + conf.config(group="amphora_agent", agent_server_network_dir=None) + + base_interface_file = ( + self.base_os_util. + get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) + self.assertEqual(base_real_nic_path, base_interface_file) + + rh_interface_file = ( + self.rh_os_util. + get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) + self.assertEqual(rh_real_nic_path, rh_interface_file) + + ubuntu_interface_file = ( + self.ubuntu_os_util. + get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) + self.assertEqual(ubuntu_real_nic_path, ubuntu_interface_file) + + def test_cmd_get_version_of_installed_package(self): + package_name = 'foo' + ubuntu_cmd = "dpkg --status {name}".format(name=package_name) + rh_cmd = "rpm -qi {name}".format(name=package_name) + + returned_ubuntu_cmd = ( + self.ubuntu_os_util.cmd_get_version_of_installed_package( + package_name)) + self.assertEqual(ubuntu_cmd, returned_ubuntu_cmd) + + returned_rh_cmd = (self.rh_os_util. + cmd_get_version_of_installed_package(package_name)) + self.assertEqual(rh_cmd, returned_rh_cmd) + + def test_has_ifup_all(self): + self.assertTrue(self.base_os_util.has_ifup_all()) + self.assertTrue(self.ubuntu_os_util.has_ifup_all()) + self.assertFalse(self.rh_os_util.has_ifup_all()) + + @mock.patch('shutil.copy2') + @mock.patch('os.makedirs') + @mock.patch('shutil.copytree') + def test_create_netns_dir(self, mock_copytree, mock_makedirs, mock_copy2): + network_dir = 'foo' + netns_network_dir = 'fake_netns_network' + ignore = shutil.ignore_patterns('fake_eth*', 'fake_loopback*') + self.rh_os_util.create_netns_dir(network_dir, + netns_network_dir, + ignore) + mock_copytree.assert_any_call( + network_dir, + os.path.join('/etc/netns/', + consts.AMPHORA_NAMESPACE, + netns_network_dir), + ignore=ignore, + symlinks=True) + + mock_makedirs.assert_any_call(os.path.join('/etc/netns/', + consts.AMPHORA_NAMESPACE)) + mock_copy2.assert_any_call( + '/etc/sysconfig/network', + '/etc/netns/{netns}/sysconfig'.format( + netns=consts.AMPHORA_NAMESPACE)) + + mock_copytree.reset_mock() + mock_makedirs.reset_mock() + mock_copy2.reset_mock() + + self.ubuntu_os_util.create_netns_dir(network_dir, + netns_network_dir, + ignore) + mock_copytree.assert_any_call( + network_dir, + os.path.join('/etc/netns/', + consts.AMPHORA_NAMESPACE, + netns_network_dir), + ignore=ignore, + symlinks=True) + + mock_makedirs.assert_any_call(os.path.join('/etc/netns/', + consts.AMPHORA_NAMESPACE)) + mock_copy2.assert_not_called() 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 f756611d89..a6cfaa155c 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 @@ -18,6 +18,7 @@ import subprocess import mock import netifaces +from octavia.amphorae.backends.agent.api_server import osutils from octavia.amphorae.backends.agent.api_server import plug import octavia.tests.unit.base as base @@ -36,7 +37,8 @@ class TestPlug(base.TestCase): def setUp(self): super(TestPlug, self).setUp() self.mock_netifaces = mock.patch.object(plug, "netifaces").start() - self.test_plug = plug.Plug() + self.osutil = osutils.BaseOS.get_os_util() + self.test_plug = plug.Plug(self.osutil) self.addCleanup(self.mock_netifaces.stop) # Set up our fake interface @@ -48,10 +50,19 @@ class TestPlug(base.TestCase): ] } - def test__interface_by_mac_case_insensitive(self): + def test__interface_by_mac_case_insensitive_ubuntu(self): interface = self.test_plug._interface_by_mac(FAKE_MAC_ADDRESS.upper()) self.assertEqual(FAKE_INTERFACE, interface) + def test__interface_by_mac_case_insensitive_rh(self): + with mock.patch('platform.linux_distribution', + return_value=['centos', 'Foo']): + osutil = osutils.BaseOS.get_os_util() + self.test_plug = plug.Plug(osutil) + interface = self.test_plug._interface_by_mac( + FAKE_MAC_ADDRESS.upper()) + self.assertEqual(FAKE_INTERFACE, interface) + @mock.patch('pyroute2.NSPopen') @mock.patch.object(plug, "flask") @mock.patch('pyroute2.IPRoute') @@ -146,9 +157,10 @@ class TestPlug(base.TestCase): class TestPlugNetwork(base.TestCase): def setUp(self): super(TestPlugNetwork, self).setUp() - self.test_plug = plug.Plug() + self.osutil = osutils.BaseOS.get_os_util() + self.test_plug = plug.Plug(self.osutil) - def test__generate_network_file_text_static_ip(self): + def test__generate_network_file_text_static_ip_ubuntu(self): netns_interface = 'eth1234' FIXED_IP = '192.0.2.2' BROADCAST = '192.0.2.255' @@ -177,8 +189,9 @@ class TestPlugNetwork(base.TestCase): 'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n' 'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n') - text = self.test_plug._generate_network_file_text(netns_interface, - fixed_ips, MTU) + template_port = osutils.j2_env.get_template('plug_port_ethX.conf.j2') + text = self.test_plug._osutils._generate_network_file_text( + netns_interface, fixed_ips, MTU, template_port) expected_text = format_text.format(netns_interface=netns_interface, fixed_ip=FIXED_IP, broadcast=BROADCAST, diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_utils.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_utils.py deleted file mode 100644 index 0f43054eb4..0000000000 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_utils.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2015 Rackspace. -# -# 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 - -from oslo_config import cfg -from oslo_config import fixture as oslo_fixture - -from octavia.amphorae.backends.agent.api_server import util -from octavia.tests.unit import base - - -class TestUtils(base.TestCase): - def setUp(self): - self.dir = '/etc/network/interfaces.d' - self.file = '/etc/network/interfaces' - - self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) - self.conf.config(group="amphora_agent", - agent_server_network_dir=self.dir) - - super(TestUtils, self).setUp() - - def test_get_network_interface_file(self): - interface = 'eth0' - - self.conf.config(group="amphora_agent", - agent_server_network_file=None) - path = util.get_network_interface_file(interface) - expected_path = os.path.join(self.dir, interface + '.cfg') - self.assertEqual(expected_path, path) - - self.conf.config(group="amphora_agent", - agent_server_network_file=self.file) - path = util.get_network_interface_file(interface) - self.assertEqual(self.file, path) diff --git a/octavia/tests/unit/common/test_utils.py b/octavia/tests/unit/common/test_utils.py index 252760d938..f76c961191 100644 --- a/octavia/tests/unit/common/test_utils.py +++ b/octavia/tests/unit/common/test_utils.py @@ -42,3 +42,9 @@ class TestConfig(base.TestCase): utils.ip_port_str('127.0.0.1', 8080)) self.assertEqual("[::1]:8080", utils.ip_port_str('::1', 8080)) + + def test_netmask_to_prefix(self): + self.assertEqual(utils.netmask_to_prefix('255.0.0.0'), 8) + self.assertEqual(utils.netmask_to_prefix('255.255.0.0'), 16) + self.assertEqual(utils.netmask_to_prefix('255.255.255.0'), 24) + self.assertEqual(utils.netmask_to_prefix('255.255.255.128'), 25) diff --git a/releasenotes/notes/add-rh-flavors-support-for-amphora-agent-cd3e9f9f519b9ff2.yaml b/releasenotes/notes/add-rh-flavors-support-for-amphora-agent-cd3e9f9f519b9ff2.yaml new file mode 100644 index 0000000000..8bb1073ea7 --- /dev/null +++ b/releasenotes/notes/add-rh-flavors-support-for-amphora-agent-cd3e9f9f519b9ff2.yaml @@ -0,0 +1,18 @@ +--- +prelude: > + Amphora image support for RH Linux flavors. +features: + - The diskimage-create script supports different operating system flavors + such as Ubuntu (the default option), CentOS, Fedora and RHEL. Adaptations + were made to several elements to ensure all images are operational. + - The amphora-agent is now able to distinguish between operating systems and + choose the right course of action to manage files and networking on each + Linux flavor. +issues: + - To use CentOS, Fedora, or RHEL in your amphora image you must set + the user_group option, located in the [haproxy_amphora] section of the + octavia.conf file to "haproxy". This will be made automatic in a + future version. +upgrade: + - agent_server_network_dir is now auto-detected for Ubuntu, CentOS, Fedora + and RHEL if one is not specified in the configuration file.