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.