238 lines
9.6 KiB
Python
238 lines
9.6 KiB
Python
# Copyright 2020 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 errno
|
|
import ipaddress
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import time
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
import pyroute2
|
|
|
|
from octavia.amphorae.backends.utils import interface_file
|
|
from octavia.common import constants as consts
|
|
from octavia.common import exceptions
|
|
|
|
CONF = cfg.CONF
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class InterfaceController(object):
|
|
ADD = 'add'
|
|
DELETE = 'delete'
|
|
SET = 'set'
|
|
|
|
def interface_file_list(self):
|
|
net_dir = interface_file.InterfaceFile.get_directory()
|
|
|
|
for f in os.listdir(net_dir):
|
|
for ext in interface_file.InterfaceFile.get_extensions():
|
|
if f.endswith(ext):
|
|
yield os.path.join(net_dir, f)
|
|
|
|
def list(self):
|
|
interfaces = {}
|
|
for f in self.interface_file_list():
|
|
iface = interface_file.InterfaceFile.from_file(f)
|
|
interfaces[iface.name] = iface
|
|
return interfaces
|
|
|
|
def _family(self, address):
|
|
return (socket.AF_INET6
|
|
if ipaddress.ip_network(address, strict=False).version == 6
|
|
else socket.AF_INET)
|
|
|
|
def _ipr_command(self, method, command,
|
|
retry_on_invalid_argument=False,
|
|
retry_interval=.2,
|
|
raise_on_error=True,
|
|
max_retries=20,
|
|
**kwargs):
|
|
|
|
for dummy in range(max_retries + 1):
|
|
try:
|
|
method(command, **kwargs)
|
|
break
|
|
except pyroute2.NetlinkError as e:
|
|
if e.code == errno.EINVAL and retry_on_invalid_argument:
|
|
LOG.debug("Retrying after %d sec.", retry_interval)
|
|
time.sleep(retry_interval)
|
|
continue
|
|
|
|
if command == self.ADD and e.code != errno.EEXIST:
|
|
msg = "Cannot call {} {} (with {}): {}".format(
|
|
method.__name__, command, kwargs, e)
|
|
if raise_on_error:
|
|
raise exceptions.AmphoraNetworkConfigException(msg)
|
|
LOG.error(msg)
|
|
return
|
|
else:
|
|
msg = "Cannot call {} {} (with {}) after {} retries.".format(
|
|
method.__name__, command, kwargs, max_retries)
|
|
if raise_on_error:
|
|
raise exceptions.AmphoraNetworkConfigException(msg)
|
|
LOG.error(msg)
|
|
|
|
def _dhclient_up(self, interface_name):
|
|
cmd = ["/sbin/dhclient",
|
|
"-lf",
|
|
"/var/lib/dhclient/dhclient-{}.leases".format(
|
|
interface_name),
|
|
"-pf",
|
|
"/run/dhclient-{}.pid".format(interface_name),
|
|
interface_name]
|
|
LOG.debug("Running '%s'", cmd)
|
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
|
|
|
def _dhclient_down(self, interface_name):
|
|
cmd = ["/sbin/dhclient",
|
|
"-r",
|
|
"-lf",
|
|
"/var/lib/dhclient/dhclient-{}.leases".format(
|
|
interface_name),
|
|
"-pf",
|
|
"/run/dhclient-{}.pid".format(interface_name),
|
|
interface_name]
|
|
LOG.debug("Running '%s'", cmd)
|
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
|
|
|
def _ipv6auto_up(self, interface_name):
|
|
# Set values to enable SLAAC on interface_name
|
|
# accept_ra is set to 2 to accept router advertisements if forwarding
|
|
# is enabled on the interface
|
|
for key, value in (('accept_ra', 2),
|
|
('autoconf', 1)):
|
|
cmd = ["/sbin/sysctl",
|
|
"-w",
|
|
"net.ipv6.conf.{}.{}={}".format(interface_name,
|
|
key, value)]
|
|
LOG.debug("Running '%s'", cmd)
|
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
|
|
|
def _ipv6auto_down(self, interface_name):
|
|
for key, value in (('accept_ra', 0),
|
|
('autoconf', 0)):
|
|
cmd = ["/sbin/sysctl",
|
|
"-w",
|
|
"net.ipv6.conf.{}.{}={}".format(interface_name,
|
|
key, value)]
|
|
LOG.debug("Running '%s'", cmd)
|
|
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
|
|
|
def up(self, interface):
|
|
LOG.info("Setting interface %s up", interface.name)
|
|
|
|
for address in interface.addresses:
|
|
if address.get(consts.DHCP):
|
|
self._dhclient_up(interface.name)
|
|
if address.get(consts.IPV6AUTO):
|
|
self._ipv6auto_up(interface.name)
|
|
|
|
with pyroute2.IPRoute() as ipr:
|
|
idx = ipr.link_lookup(ifname=interface.name)[0]
|
|
|
|
self._ipr_command(ipr.link, self.SET, index=idx,
|
|
state=consts.IFACE_UP, mtu=interface.mtu)
|
|
|
|
for address in interface.addresses:
|
|
if (consts.ADDRESS not in address or
|
|
address.get(consts.DHCP) or
|
|
address.get(consts.IPV6AUTO)):
|
|
continue
|
|
address[consts.FAMILY] = self._family(address[consts.ADDRESS])
|
|
LOG.debug("%s: Adding address %s", interface.name, address)
|
|
self._ipr_command(ipr.addr, self.ADD, index=idx, **address)
|
|
|
|
for route in interface.routes:
|
|
route[consts.FAMILY] = self._family(route[consts.DST])
|
|
LOG.debug("%s: Adding route %s", interface.name, route)
|
|
# Set retry_on_invalid_argument=True because the interface
|
|
# might not be ready after setting its addresses
|
|
# Note: can we use 'replace' instead of 'add' here?
|
|
# Set raise_on_error to False, possible invalid (user-defined)
|
|
# routes from the subnet's host_routes will not break the
|
|
# script.
|
|
self._ipr_command(ipr.route, self.ADD,
|
|
retry_on_invalid_argument=True,
|
|
raise_on_error=False,
|
|
oif=idx, **route)
|
|
|
|
for rule in interface.rules:
|
|
rule[consts.FAMILY] = self._family(rule[consts.SRC])
|
|
LOG.debug("%s: Adding rule %s", interface.name, rule)
|
|
self._ipr_command(ipr.rule, self.ADD,
|
|
retry_on_invalid_argument=True,
|
|
**rule)
|
|
|
|
for script in interface.scripts[consts.IFACE_UP]:
|
|
LOG.debug("%s: Running command '%s'",
|
|
interface.name, script[consts.COMMAND])
|
|
subprocess.check_output(script[consts.COMMAND].split())
|
|
|
|
def down(self, interface):
|
|
LOG.info("Setting interface %s down", interface.name)
|
|
|
|
for address in interface.addresses:
|
|
if address.get(consts.DHCP):
|
|
self._dhclient_down(interface.name)
|
|
if address.get(consts.IPV6AUTO):
|
|
self._ipv6auto_down(interface.name)
|
|
|
|
with pyroute2.IPRoute() as ipr:
|
|
idx = ipr.link_lookup(ifname=interface.name)[0]
|
|
|
|
link = ipr.get_links(idx)[0]
|
|
current_state = link.get(consts.STATE)
|
|
|
|
if current_state == consts.IFACE_UP:
|
|
for rule in interface.rules:
|
|
rule[consts.FAMILY] = self._family(rule[consts.SRC])
|
|
LOG.debug("%s: Deleting rule %s", interface.name, rule)
|
|
self._ipr_command(ipr.rule, self.DELETE,
|
|
raise_on_error=False, **rule)
|
|
|
|
for route in interface.routes:
|
|
route[consts.FAMILY] = self._family(route[consts.DST])
|
|
LOG.debug("%s: Deleting route %s", interface.name, route)
|
|
self._ipr_command(ipr.route, self.DELETE,
|
|
raise_on_error=False, oif=idx, **route)
|
|
|
|
for address in interface.addresses:
|
|
if consts.ADDRESS not in address:
|
|
continue
|
|
address[consts.FAMILY] = self._family(
|
|
address[consts.ADDRESS])
|
|
LOG.debug("%s: Deleting address %s",
|
|
interface.name, address)
|
|
self._ipr_command(ipr.addr, self.DELETE,
|
|
raise_on_error=False,
|
|
index=idx, **address)
|
|
|
|
self._ipr_command(ipr.link, self.SET, raise_on_error=False,
|
|
index=idx, state=consts.IFACE_DOWN)
|
|
|
|
if current_state == consts.IFACE_UP:
|
|
for script in interface.scripts[consts.IFACE_DOWN]:
|
|
LOG.debug("%s: Running command '%s'",
|
|
interface.name, script[consts.COMMAND])
|
|
try:
|
|
subprocess.check_output(script[consts.COMMAND].split())
|
|
except Exception as e:
|
|
LOG.error("Error while running command '%s' on %s: %s",
|
|
script[consts.COMMAND], interface.name, e)
|