307 lines
12 KiB
Python
307 lines
12 KiB
Python
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
# Copyright 2016 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 ipaddress
|
|
import itertools
|
|
import os
|
|
import socket
|
|
import stat
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
import pyroute2
|
|
import webob
|
|
from werkzeug import exceptions
|
|
|
|
from octavia.amphorae.backends.agent.api_server import util
|
|
from octavia.common import constants as consts
|
|
|
|
|
|
CONF = cfg.CONF
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class Plug(object):
|
|
def __init__(self, osutils):
|
|
self._osutils = osutils
|
|
|
|
def plug_lo(self):
|
|
self._osutils.write_interface_file(
|
|
interface="lo",
|
|
ip_address="127.0.0.1",
|
|
prefixlen=8)
|
|
|
|
def render_vips(self, vips):
|
|
rendered_vips = []
|
|
for vip in vips:
|
|
ip_address = ipaddress.ip_address(vip['ip_address'])
|
|
subnet_cidr = ipaddress.ip_network(vip['subnet_cidr'])
|
|
prefixlen = subnet_cidr.prefixlen
|
|
host_routes = vip['host_routes']
|
|
gateway = vip['gateway']
|
|
rendered_vips.append({
|
|
'ip_address': ip_address.exploded,
|
|
'ip_version': ip_address.version,
|
|
'gateway': gateway,
|
|
'host_routes': host_routes,
|
|
'prefixlen': prefixlen
|
|
})
|
|
return rendered_vips
|
|
|
|
def build_vrrp_info(self, vrrp_ip, subnet_cidr, gateway, host_routes):
|
|
vrrp_info = {}
|
|
if vrrp_ip:
|
|
ip_address = ipaddress.ip_address(vrrp_ip)
|
|
subnet_cidr = ipaddress.ip_network(subnet_cidr)
|
|
prefixlen = subnet_cidr.prefixlen
|
|
vrrp_info.update({
|
|
'ip': ip_address.exploded,
|
|
'ip_version': ip_address.version,
|
|
'gateway': gateway,
|
|
'host_routes': host_routes,
|
|
'prefixlen': prefixlen
|
|
})
|
|
return vrrp_info
|
|
|
|
def plug_vip(self, vip, subnet_cidr, gateway,
|
|
mac_address, mtu=None, vrrp_ip=None, host_routes=(),
|
|
additional_vips=(), is_sriov=False):
|
|
vips = [{
|
|
'ip_address': vip,
|
|
'subnet_cidr': subnet_cidr,
|
|
'gateway': gateway,
|
|
'host_routes': host_routes
|
|
}] + list(additional_vips)
|
|
|
|
try:
|
|
rendered_vips = self.render_vips(vips)
|
|
except ValueError as e:
|
|
vip_error_message = "Invalid VIP: {}".format(e)
|
|
return webob.Response(json={'message': vip_error_message},
|
|
status=400)
|
|
|
|
try:
|
|
vrrp_info = self.build_vrrp_info(vrrp_ip, subnet_cidr,
|
|
gateway, host_routes)
|
|
except ValueError as e:
|
|
return webob.Response(
|
|
json={'message': "Invalid VRRP Address: {}".format(e)},
|
|
status=400)
|
|
|
|
# Check if the interface is already in the network namespace
|
|
# Do not attempt to re-plug the VIP if it is already in the
|
|
# network namespace
|
|
if self._netns_interface_exists(mac_address):
|
|
return webob.Response(
|
|
json={'message': "Interface already exists"}, status=409)
|
|
|
|
# Check that the interface has been fully plugged
|
|
self._interface_by_mac(mac_address)
|
|
|
|
# Always put the VIP interface as eth1
|
|
primary_interface = consts.NETNS_PRIMARY_INTERFACE
|
|
|
|
self._osutils.write_vip_interface_file(
|
|
interface=primary_interface,
|
|
vips=rendered_vips,
|
|
mtu=mtu,
|
|
vrrp_info=vrrp_info,
|
|
is_sriov=is_sriov)
|
|
|
|
# Update the list of interfaces to add to the namespace
|
|
# This is used in the amphora reboot case to re-establish the namespace
|
|
self._update_plugged_interfaces_file(primary_interface, mac_address)
|
|
|
|
with pyroute2.IPRoute() as ipr:
|
|
# Move the interfaces into the namespace
|
|
idx = ipr.link_lookup(address=mac_address)[0]
|
|
ipr.link('set', index=idx, net_ns_fd=consts.AMPHORA_NAMESPACE,
|
|
IFLA_IFNAME=primary_interface)
|
|
|
|
# bring interfaces up
|
|
self._osutils.bring_interface_up(primary_interface, 'VIP')
|
|
|
|
vip_message = "VIPs plugged on interface {interface}: {vips}".format(
|
|
interface=primary_interface,
|
|
vips=", ".join(v['ip_address'] for v in rendered_vips)
|
|
)
|
|
|
|
return webob.Response(json={
|
|
'message': "OK",
|
|
'details': vip_message}, status=202)
|
|
|
|
def _check_ip_addresses(self, fixed_ips):
|
|
if fixed_ips:
|
|
for ip in fixed_ips:
|
|
try:
|
|
socket.inet_pton(socket.AF_INET, ip.get('ip_address'))
|
|
except socket.error:
|
|
socket.inet_pton(socket.AF_INET6, ip.get('ip_address'))
|
|
|
|
def plug_network(self, mac_address, fixed_ips, mtu=None,
|
|
vip_net_info=None):
|
|
try:
|
|
self._check_ip_addresses(fixed_ips=fixed_ips)
|
|
except socket.error:
|
|
return webob.Response(json={
|
|
'message': "Invalid network port"}, status=400)
|
|
|
|
# Check if the interface is already in the network namespace
|
|
# Do not attempt to re-plug the network if it is already in the
|
|
# network namespace, just ensure all fixed_ips are up
|
|
if self._netns_interface_exists(mac_address):
|
|
# Get the existing interface name and path
|
|
existing_interface = self._netns_interface_by_mac(mac_address)
|
|
|
|
# If we have net_info, this is the special case of plugging a new
|
|
# subnet on the vrrp port, which is essentially a re-vip-plug
|
|
if vip_net_info:
|
|
vrrp_ip = vip_net_info.get('vrrp_ip')
|
|
subnet_cidr = vip_net_info['subnet_cidr']
|
|
gateway = vip_net_info['gateway']
|
|
host_routes = vip_net_info.get('host_routes', [])
|
|
|
|
vips = [{
|
|
'ip_address': vip_net_info['vip'],
|
|
'subnet_cidr': subnet_cidr,
|
|
'gateway': gateway,
|
|
'host_routes': host_routes
|
|
}] + vip_net_info.get('additional_vips', [])
|
|
rendered_vips = self.render_vips(vips)
|
|
vrrp_info = self.build_vrrp_info(vrrp_ip, subnet_cidr,
|
|
gateway, host_routes)
|
|
|
|
self._osutils.write_vip_interface_file(
|
|
interface=existing_interface,
|
|
vips=rendered_vips,
|
|
mtu=mtu,
|
|
vrrp_info=vrrp_info,
|
|
fixed_ips=fixed_ips)
|
|
self._osutils.bring_interface_up(existing_interface, 'vip')
|
|
# Otherwise, we are just plugging a run-of-the-mill network
|
|
else:
|
|
# Write an updated config
|
|
self._osutils.write_port_interface_file(
|
|
interface=existing_interface,
|
|
fixed_ips=fixed_ips,
|
|
mtu=mtu)
|
|
self._osutils.bring_interface_up(existing_interface, 'network')
|
|
|
|
util.send_member_advertisements(fixed_ips)
|
|
return webob.Response(json={
|
|
'message': "OK",
|
|
'details': "Updated existing interface {interface}".format(
|
|
# TODO(rm_work): Everything in this should probably use
|
|
# HTTP code 200, but continuing to use 202 for consistency.
|
|
interface=existing_interface)}, status=202)
|
|
|
|
# This is the interface as it was initially plugged into the
|
|
# default network namespace, this will likely always be eth1
|
|
default_netns_interface = self._interface_by_mac(mac_address)
|
|
|
|
# We need to determine the interface name when inside the namespace
|
|
# to avoid name conflicts
|
|
netns_interface = self._netns_get_next_interface()
|
|
|
|
LOG.info('Plugged interface %s will become %s in the namespace %s',
|
|
default_netns_interface, netns_interface,
|
|
consts.AMPHORA_NAMESPACE)
|
|
self._osutils.write_port_interface_file(
|
|
interface=netns_interface,
|
|
fixed_ips=fixed_ips,
|
|
mtu=mtu)
|
|
|
|
# Update the list of interfaces to add to the namespace
|
|
self._update_plugged_interfaces_file(netns_interface, mac_address)
|
|
|
|
with pyroute2.IPRoute() as ipr:
|
|
# Move the interfaces into the namespace
|
|
idx = ipr.link_lookup(address=mac_address)[0]
|
|
ipr.link('set', index=idx,
|
|
net_ns_fd=consts.AMPHORA_NAMESPACE,
|
|
IFLA_IFNAME=netns_interface)
|
|
|
|
self._osutils.bring_interface_up(netns_interface, 'network')
|
|
util.send_member_advertisements(fixed_ips)
|
|
|
|
return webob.Response(json={
|
|
'message': "OK",
|
|
'details': "Plugged on interface {interface}".format(
|
|
interface=netns_interface)}, status=202)
|
|
|
|
def _interface_by_mac(self, mac):
|
|
try:
|
|
with pyroute2.IPRoute() as ipr:
|
|
idx = ipr.link_lookup(address=mac)[0]
|
|
# Workaround for https://github.com/PyCQA/pylint/issues/8497
|
|
# pylint: disable=E1136, E1121
|
|
addr = ipr.get_links(idx)[0]
|
|
for attr in addr['attrs']:
|
|
if attr[0] == consts.IFLA_IFNAME:
|
|
return attr[1]
|
|
except Exception as e:
|
|
LOG.info('Unable to find interface with MAC: %s, rescanning '
|
|
'and returning 404. Reported error: %s', mac, str(e))
|
|
|
|
# Poke the kernel to re-enumerate the PCI bus.
|
|
# We have had cases where nova hot plugs the interface but
|
|
# the kernel doesn't get the memo.
|
|
filename = '/sys/bus/pci/rescan'
|
|
flags = os.O_WRONLY
|
|
if os.path.isfile(filename):
|
|
with os.fdopen(os.open(filename, flags), 'w') as rescan_file:
|
|
rescan_file.write('1')
|
|
raise exceptions.HTTPException(
|
|
response=webob.Response(json={
|
|
'details': "No suitable network interface found"}, status=404))
|
|
|
|
def _update_plugged_interfaces_file(self, interface, mac_address):
|
|
# write interfaces to plugged_interfaces file and prevent duplicates
|
|
plug_inf_file = consts.PLUGGED_INTERFACES
|
|
flags = os.O_RDWR | os.O_CREAT
|
|
# mode 0644
|
|
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
|
|
with os.fdopen(os.open(plug_inf_file, flags, mode), 'r+') as text_file:
|
|
inf_list = [inf.split()[0].rstrip() for inf in text_file]
|
|
if mac_address not in inf_list:
|
|
text_file.write("{mac_address} {interface}\n".format(
|
|
mac_address=mac_address, interface=interface))
|
|
|
|
def _netns_interface_by_mac(self, mac_address):
|
|
with pyroute2.NetNS(consts.AMPHORA_NAMESPACE,
|
|
flags=os.O_CREAT) as netns:
|
|
for link in netns.get_links():
|
|
attr_dict = dict(link['attrs'])
|
|
if attr_dict.get(consts.IFLA_ADDRESS) == mac_address:
|
|
return attr_dict.get(consts.IFLA_IFNAME)
|
|
return None
|
|
|
|
def _netns_interface_exists(self, mac_address):
|
|
return self._netns_interface_by_mac(mac_address) is not None
|
|
|
|
def _netns_get_next_interface(self):
|
|
with pyroute2.NetNS(consts.AMPHORA_NAMESPACE,
|
|
flags=os.O_CREAT) as netns:
|
|
existing_ifaces = [
|
|
dict(link['attrs']).get(consts.IFLA_IFNAME)
|
|
for link in netns.get_links()]
|
|
# find the first unused ethXXX
|
|
for idx in itertools.count(start=2):
|
|
iface_name = f"eth{idx}"
|
|
if iface_name not in existing_ifaces:
|
|
break
|
|
return iface_name
|