sonic-buildimage/files/image_config/hostcfgd/hostcfgd
abdosi ab05a2f58a
Add support for BGP Monitors on multi asic SONiC platforms. (#6977)
This PR is cherry-pick of master
https://github.com/Azure/sonic-buildimage/pull/6920

Why I did it
Add support for BGP Monitors on multi asic SONiC platforms.

How I did it
On multi ASIC SONiC platforms, BGP monitor session will be established from Backend ASIC.
To achieve this following changes are done

Add BGP monitor configuration on the backend ASIC.
The BGP monitor configuration is present in the DPG of the device in minigraph.xml of multi-ASIC device, so this configuration will be added to the config_db of the host, when the minigraph is loaded.
To add configuration for this in the Backend ASIC, a new class MultiAsicBgpMonCfg is added to the hostcfgd service to update the config_db of the backend ASIC when the BGP_MONITOR table of the host config_db is updated.
This way incremental BGP_MONITOR configuration can also be handled.

Changes to establish BGP session with bgp monitor.

Add route in host main routing table to go to one of pre-define backend asic
Add IP table rule on front asic to mark the BGP packets with destination as IPv4 Loopback.
Add IP rule in front asic namespace to match mark BGP packet and lookup default table
Program the default route in FrontEnd asic name space docker default table as part of start.sh of the BGP container.
It need to be done as part of start.sh otherwise FRR default route will get over-written.
How to verify it

Signed-off-by: Abhishek Dosi <abdosi@microsoft.com>
Co-authored-by: Arvind <arlakshm@microsoft.com>
2021-03-06 21:21:52 -08:00

550 lines
23 KiB
Python
Executable File

#!/usr/bin/python -u
# -*- coding: utf-8 -*-
import ast
import os
import time
import sys
import subprocess
import syslog
import copy
import jinja2
import ipaddr as ipaddress
from swsssdk import ConfigDBConnector, SonicDBConfig
from sonic_py_common import device_info,multi_asic
# FILE
PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
PAM_AUTH_CONF_TEMPLATE = "/usr/share/sonic/templates/common-auth-sonic.j2"
NSS_TACPLUS_CONF = "/etc/tacplus_nss.conf"
NSS_TACPLUS_CONF_TEMPLATE = "/usr/share/sonic/templates/tacplus_nss.conf.j2"
NSS_CONF = "/etc/nsswitch.conf"
# TACACS+
TACPLUS_SERVER_PASSKEY_DEFAULT = ""
TACPLUS_SERVER_TIMEOUT_DEFAULT = "5"
TACPLUS_SERVER_AUTH_TYPE_DEFAULT = "pap"
def is_true(val):
if val == 'True' or val == 'true':
return True
else:
return False
def sub(l, start, end):
return l[start:end]
def obfuscate(data):
if data:
return data[0] + '*****'
else:
return data
class Iptables(object):
def __init__(self):
'''
Default MSS to 1460 - (MTU 1500 - 40 (TCP/IP Overhead))
For IPv6, it would be 1440 - (MTU 1500 - 60 octects)
'''
self.tcpmss = 1460
self.tcp6mss = 1440
def is_ip_prefix_in_key(self, key):
'''
Function to check if IP address is present in the key. If it
is present, then the key would be a tuple or else, it shall be
be string
'''
return (isinstance(key, tuple))
def load(self, lpbk_table):
for row in lpbk_table:
self.iptables_tcpmss_handler(row, lpbk_table[row])
def command(self, chain, ip, ver, op):
cmd = 'iptables' if ver == '4' else 'ip6tables'
cmd += ' -t mangle --{} {} -p tcp --tcp-flags SYN SYN'.format(op, chain)
cmd += ' -d' if chain == 'PREROUTING' else ' -s'
mss = self.tcpmss if ver == '4' else self.tcp6mss
cmd += ' {} -j TCPMSS --set-mss {}'.format(ip, mss)
return cmd
def iptables_tcpmss_handler(self, key, data, add=True):
if not self.is_ip_prefix_in_key(key):
return
iface, ip = key
ip_str = ip.split("/")[0]
ip_addr = ipaddress.IPAddress(ip_str)
if isinstance(ip_addr, ipaddress.IPv6Address):
ver = '6'
else:
ver = '4'
iptables_tcpmss_cmds = []
for chain in ['PREROUTING', 'POSTROUTING']:
iptables_tcpmss_cmds.append(self.command(chain, ip_str, ver, 'delete' if not add else 'check'))
self.mangle_handler(iptables_tcpmss_cmds, add)
def mangle_handler(self, cmds, add):
iptables_cmds = []
if add:
for cmd in cmds:
'''
For add case, first check if rule exists. Iptables just appends to the chain
as a new rule even if it is the same as an existing one. Check this and
do nothing if rule exists
'''
ret = subprocess.call(cmd, shell=True)
if ret == 0:
syslog.syslog(syslog.LOG_INFO, "{} rule exists".format(cmd))
else:
# Modify command from Check to Append
iptables_cmds.append(cmd.replace("check", "append"))
else:
iptables_cmds = cmds
for cmd in iptables_cmds:
syslog.syslog(syslog.LOG_INFO, "Running cmd - {}".format(cmd))
try:
subprocess.check_call(cmd, shell=True)
except subprocess.CalledProcessError as err:
syslog.syslog(syslog.LOG_ERR, "'{}' failed. RC: {}, output: {}"
.format(err.cmd, err.returncode, err.output))
class AaaCfg(object):
def __init__(self):
self.auth_default = {
'login': 'local',
}
self.tacplus_global_default = {
'auth_type': TACPLUS_SERVER_AUTH_TYPE_DEFAULT,
'timeout': TACPLUS_SERVER_TIMEOUT_DEFAULT,
'passkey': TACPLUS_SERVER_PASSKEY_DEFAULT
}
self.auth = {}
self.tacplus_global = {}
self.tacplus_servers = {}
self.debug = False
# Load conf from ConfigDb
def load(self, aaa_conf, tac_global_conf, tacplus_conf):
for row in aaa_conf:
self.aaa_update(row, aaa_conf[row], modify_conf=False)
for row in tac_global_conf:
self.tacacs_global_update(row, tac_global_conf[row], modify_conf=False)
for row in tacplus_conf:
self.tacacs_server_update(row, tacplus_conf[row], modify_conf=False)
self.modify_conf_file()
def aaa_update(self, key, data, modify_conf=True):
if key == 'authentication':
self.auth = data
if 'failthrough' in data:
self.auth['failthrough'] = is_true(data['failthrough'])
if 'debug' in data:
self.debug = is_true(data['debug'])
if modify_conf:
self.modify_conf_file()
def tacacs_global_update(self, key, data, modify_conf=True):
if key == 'global':
self.tacplus_global = data
if modify_conf:
self.modify_conf_file()
def tacacs_server_update(self, key, data, modify_conf=True):
if data == {}:
if key in self.tacplus_servers:
del self.tacplus_servers[key]
else:
self.tacplus_servers[key] = data
if modify_conf:
self.modify_conf_file()
def modify_single_file(self, filename, operations=None):
if operations:
cmd = "sed -e {0} {1} > {1}.new; mv -f {1} {1}.old; mv -f {1}.new {1}".format(' -e '.join(operations), filename)
os.system(cmd)
def modify_conf_file(self):
auth = self.auth_default.copy()
auth.update(self.auth)
tacplus_global = self.tacplus_global_default.copy()
tacplus_global.update(self.tacplus_global)
servers_conf = []
if self.tacplus_servers:
for addr in self.tacplus_servers:
server = tacplus_global.copy()
server['ip'] = addr
server.update(self.tacplus_servers[addr])
servers_conf.append(server)
servers_conf = sorted(servers_conf, key=lambda t: int(t['priority']), reverse=True)
template_file = os.path.abspath(PAM_AUTH_CONF_TEMPLATE)
env = jinja2.Environment(loader=jinja2.FileSystemLoader('/'), trim_blocks=True)
env.filters['sub'] = sub
template = env.get_template(template_file)
pam_conf = template.render(auth=auth, servers=servers_conf)
with open(PAM_AUTH_CONF, 'w') as f:
f.write(pam_conf)
# Modify common-auth include file in /etc/pam.d/login and sshd
if os.path.isfile(PAM_AUTH_CONF):
self.modify_single_file('/etc/pam.d/sshd', [ "'/^@include/s/common-auth$/common-auth-sonic/'" ])
self.modify_single_file('/etc/pam.d/login', [ "'/^@include/s/common-auth$/common-auth-sonic/'" ])
else:
self.modify_single_file('/etc/pam.d/sshd', [ "'/^@include/s/common-auth-sonic$/common-auth/'" ])
self.modify_single_file('/etc/pam.d/login', [ "'/^@include/s/common-auth-sonic$/common-auth/'" ])
# Add tacplus in nsswitch.conf if TACACS+ enable
if 'tacacs+' in auth['login']:
if os.path.isfile(NSS_CONF):
self.modify_single_file(NSS_CONF, [ "'/tacplus/b'", "'/^passwd/s/compat/tacplus &/'"])
else:
if os.path.isfile(NSS_CONF):
self.modify_single_file(NSS_CONF, [ "'/^passwd/s/tacplus //'" ])
# Set tacacs+ server in nss-tacplus conf
template_file = os.path.abspath(NSS_TACPLUS_CONF_TEMPLATE)
template = env.get_template(template_file)
nss_tacplus_conf = template.render(debug=self.debug, servers=servers_conf)
with open(NSS_TACPLUS_CONF, 'w') as f:
f.write(nss_tacplus_conf)
class MultiAsicBgpMonCfg(object):
def __init__(self):
self.ns_for_bgp_mon = 'asic4'
ip_address_get_command = "ip -n {} -4 -o addr show eth0".format(self.ns_for_bgp_mon) +\
" | awk '{print $4}' | cut -d'/' -f1 | head -1"
self.ns_docker_ip_fo_bgp_mon = self.run_ip_commands([ip_address_get_command])
self.bgp_monitor_table = 'BGP_MONITORS'
self.config_db = ConfigDBConnector()
self.config_db.connect()
SonicDBConfig.load_sonic_global_db_config()
self.backend_config_db = ConfigDBConnector(namespace=self.ns_for_bgp_mon)
self.backend_config_db.connect()
self.frontend_namespace = multi_asic.get_front_end_namespaces()
self.bgp_listen_port = 179
self.bgp_fw_mark = 1
self.iptable_handler = Iptables()
def run_ip_commands(self, commands):
"""
Given a list of shell ip commands, run them in order
Args:
commands: List of strings, each string is a shell command
"""
for cmd in commands:
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = proc.communicate()
if proc.returncode != 0 and stderr.rstrip('\n') != "RTNETLINK answers: File exists":
syslog.syslog(syslog.LOG_ERR,"Error running command '{}'".format(cmd))
elif stdout:
return stdout.rstrip('\n')
def _update_bgp_mon_backend_asic(self, key, data):
op = "added" if data is not None else "deleted"
syslog.syslog(syslog.LOG_INFO,
'{} bgp mon{} on {}'.format(op, key,
self.ns_for_bgp_mon))
self.backend_config_db.set_entry(self.bgp_monitor_table, key, data)
def get_lo0_ipv4(self, lpbk_table):
"""
Extract Loopback0 ipv4 address from the Directory
:return: ipv4 address for Loopback0, None if nothing found
"""
for loopback in lpbk_table.iterkeys():
if loopback[0].startswith("Loopback0"):
if loopback[1] and isinstance(ipaddress.IPAddress(loopback[1].split("/")[0]), ipaddress.IPv4Address):
return loopback[1].split("/")[0]
return None
def load(self, lpbk_table):
'''
This function loads the bgp mon configuration from the host
config_db to a backend config_db.
It also Add the ip rule and default route in host to take BGP packet
to the ASIC running BGPMON
'''
loopback0_ipv4 = self.get_lo0_ipv4(lpbk_table)
if loopback0_ipv4:
bgpmon_ip_cmd = []
bgpmon_iptable_cmd = []
'''
Add route in host main routing table for Loopback IP to go to pre-define bgpmon backend asic
'''
bgpmon_ip_cmd.append("ip route add {}/32 via {} dev docker0".format(loopback0_ipv4, self.ns_docker_ip_fo_bgp_mon))
for front_ns in self.frontend_namespace:
'''
Add ip table rule on front asic to mark the BGP packets for destination as Loopback IP
'''
bgpmon_iptable_cmd.append("ip netns exec {} iptables -t mangle -d {}/32 -p tcp --sport {} --check PREROUTING -j MARK --set-mark {}"
.format(front_ns, loopback0_ipv4, self.bgp_listen_port, self.bgp_fw_mark))
bgpmon_iptable_cmd.append("ip netns exec {} iptables -t mangle -d {}/32 -p tcp --dport {} --check PREROUTING -j MARK --set-mark {}"
.format(front_ns, loopback0_ipv4, self.bgp_listen_port, self.bgp_fw_mark))
'''
Add ip rule in front asic namespace to match mark packet and lookup default table
'''
bgpmon_ip_cmd.append("ip -n {} rule add fwmark {} pref 101 lookup default".format(front_ns, self.bgp_fw_mark))
self.run_ip_commands(bgpmon_ip_cmd)
self.iptable_handler.mangle_handler(bgpmon_iptable_cmd, True)
else:
'''
Log error as IPv4 Loopback0 Address not present
'''
syslog.syslog(syslog.LOG_ERR, "Loopback0 IPv4 does not exist")
host_bgp_mon_table = self.config_db.get_table(self.bgp_monitor_table)
if host_bgp_mon_table:
for key in host_bgp_mon_table:
data = host_bgp_mon_table[key]
self._update_bgp_mon_backend_asic(key, data)
self.backend_config_db.set_entry(self.bgp_monitor_table, key,
data)
else:
'''
the host config_db has no bgp monitor configuration.
delete the configuration from the backend asic, if present.
'''
bkend_bgp_mon_table = self.backend_config_db.get_table(self.bgp_monitor_table)
if bkend_bgp_mon_table:
self.backend_config_db.delete_table(self.bgp_monitor_table)
def cfg_handler(self, key, data):
key = ConfigDBConnector.deserialize_key(key)
keys = self.config_db.get_keys(self.bgp_monitor_table)
if key not in keys:
data = None
self._update_bgp_mon_backend_asic(key, data)
class HostConfigDaemon:
def __init__(self):
self.config_db = ConfigDBConnector()
self.config_db.connect(wait_for_init=True, retry_on=True)
syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success')
self.aaacfg = AaaCfg()
self.iptables = Iptables()
# Cache the values of 'state' field in 'FEATURE' table of each container
self.cached_feature_states = {}
self.is_multi_npu = device_info.is_multi_npu()
# Default is None. Gets initialize for multi-npu platforms
self.masicBgpMonCfg = None
if self.is_multi_npu:
self.masicBgpMonCfg = MultiAsicBgpMonCfg()
def load(self):
aaa = self.config_db.get_table('AAA')
tacacs_global = self.config_db.get_table('TACPLUS')
tacacs_server = self.config_db.get_table('TACPLUS_SERVER')
self.aaacfg.load(aaa, tacacs_global, tacacs_server)
lpbk_table = self.config_db.get_table('LOOPBACK_INTERFACE')
self.iptables.load(lpbk_table)
if self.masicBgpMonCfg:
self.masicBgpMonCfg.load(lpbk_table)
def masic_bgp_mon_cfg_handler(self, key, data):
if self.masicBgpMonCfg:
syslog.syslog(syslog.LOG_INFO, 'Multi asic bgp cfg handler...')
self.masicBgpMonCfg.cfg_handler(key, data)
def wait_till_system_init_done(self):
systemctl_cmd = "sudo systemctl is-system-running"
while True:
proc = subprocess.Popen(
systemctl_cmd,
shell=True,
stdout=subprocess.PIPE,
)
output = proc.communicate()[0].rstrip('\n')
if output.lower() in ["initializing", "starting"]:
time.sleep(1)
continue
return
def update_feature_state(self, feature_name, state, feature_table):
if state == "always_enabled":
syslog.syslog(syslog.LOG_INFO, "Feature '{}' service is always enabled"
.format(feature_name))
return
has_timer = ast.literal_eval(feature_table[feature_name].get('has_timer', 'False'))
has_global_scope = ast.literal_eval(feature_table[feature_name].get('has_global_scope', 'True'))
has_per_asic_scope = ast.literal_eval(feature_table[feature_name].get('has_per_asic_scope', 'False'))
# Create feature name suffix depending feature is running in host or namespace or in both
feature_name_suffix_list = (([feature_name] if has_global_scope or not self.is_multi_npu else []) +
([(feature_name + '@' + str(asic_inst)) for asic_inst in range(device_info.get_num_npus())
if has_per_asic_scope and self.is_multi_npu]))
if not feature_name_suffix_list:
syslog.syslog(syslog.LOG_ERR, "Feature '{}' service not available"
.format(feature_name))
feature_suffixes = ["service"] + (["timer"] if has_timer else [])
if state == "enabled":
start_cmds = []
for feature_name_suffix in feature_name_suffix_list:
for suffix in feature_suffixes:
start_cmds.append("sudo systemctl unmask {}.{}".format(feature_name_suffix, suffix))
# If feature has timer associated with it, start/enable corresponding systemd .timer unit
# otherwise, start/enable corresponding systemd .service unit
start_cmds.append("sudo systemctl enable {}.{}".format(feature_name_suffix, feature_suffixes[-1]))
start_cmds.append("sudo systemctl start {}.{}".format(feature_name_suffix, feature_suffixes[-1]))
for cmd in start_cmds:
syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd))
try:
subprocess.check_call(cmd, shell=True)
except subprocess.CalledProcessError as err:
syslog.syslog(syslog.LOG_ERR, "'{}' failed. RC: {}, output: {}"
.format(err.cmd, err.returncode, err.output))
syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be enabled and started"
.format(feature_name, feature_suffixes[-1]))
return
syslog.syslog(syslog.LOG_INFO, "Feature '{}.{}' is enabled and started"
.format(feature_name, feature_suffixes[-1]))
elif state == "disabled":
stop_cmds = []
for feature_name_suffix in feature_name_suffix_list:
for suffix in reversed(feature_suffixes):
stop_cmds.append("sudo systemctl stop {}.{}".format(feature_name_suffix, suffix))
stop_cmds.append("sudo systemctl disable {}.{}".format(feature_name_suffix, suffix))
stop_cmds.append("sudo systemctl mask {}.{}".format(feature_name_suffix, suffix))
for cmd in stop_cmds:
syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd))
try:
subprocess.check_call(cmd, shell=True)
except subprocess.CalledProcessError as err:
syslog.syslog(syslog.LOG_ERR, "'{}' failed. RC: {}, output: {}"
.format(err.cmd, err.returncode, err.output))
syslog.syslog(syslog.LOG_ERR, "Feature '{}' failed to be stopped and disabled".format(feature_name))
return
syslog.syslog(syslog.LOG_INFO, "Feature '{}' is stopped and disabled".format(feature_name))
else:
syslog.syslog(syslog.LOG_ERR, "Unexpected state value '{}' for feature '{}'"
.format(state, feature_name))
def update_all_feature_states(self):
feature_table = self.config_db.get_table('FEATURE')
for feature_name in feature_table.keys():
if not feature_name:
syslog.syslog(syslog.LOG_WARNING, "Feature is None")
continue
state = feature_table[feature_name]['state']
if not state:
syslog.syslog(syslog.LOG_WARNING, "Eanble state of feature '{}' is None".format(feature_name))
continue
# Store the initial value of 'state' field in 'FEATURE' table of a specific container
self.cached_feature_states[feature_name] = state
self.update_feature_state(feature_name, state, feature_table)
def aaa_handler(self, key, data):
self.aaacfg.aaa_update(key, data)
def tacacs_server_handler(self, key, data):
self.aaacfg.tacacs_server_update(key, data)
log_data = copy.deepcopy(data)
if 'passkey' in log_data:
log_data['passkey'] = obfuscate(log_data['passkey'])
syslog.syslog(syslog.LOG_INFO, 'value of {} changed to {}'.format(key, log_data))
def tacacs_global_handler(self, key, data):
self.aaacfg.tacacs_global_update(key, data)
log_data = copy.deepcopy(data)
if 'passkey' in log_data:
log_data['passkey'] = obfuscate(log_data['passkey'])
syslog.syslog(syslog.LOG_INFO, 'value of {} changed to {}'.format(key, log_data))
def lpbk_handler(self, key, data):
key = ConfigDBConnector.deserialize_key(key)
# Check if delete operation by fetch existing keys
keys = self.config_db.get_keys('LOOPBACK_INTERFACE')
if key in keys:
add = True
else:
add = False
self.iptables.iptables_tcpmss_handler(key, data, add)
def feature_state_handler(self, key, data):
feature_name = key
feature_table = self.config_db.get_table('FEATURE')
if feature_name not in feature_table.keys():
syslog.syslog(syslog.LOG_WARNING, "Feature '{}' not in FEATURE table".format(feature_name))
return
state = feature_table[feature_name]['state']
if not state:
syslog.syslog(syslog.LOG_WARNING, "Enable state of feature '{}' is None".format(feature_name))
return
self.cached_feature_states.setdefault(feature_name, 'disabled')
# Enable/disable the container service if the feature state was changed from its previous state.
if self.cached_feature_states[feature_name] != state:
self.cached_feature_states[feature_name] = state
self.update_feature_state(feature_name, state, feature_table)
def start(self):
self.config_db.subscribe('AAA', lambda table, key, data: self.aaa_handler(key, data))
self.config_db.subscribe('TACPLUS_SERVER', lambda table, key, data: self.tacacs_server_handler(key, data))
self.config_db.subscribe('TACPLUS', lambda table, key, data: self.tacacs_global_handler(key, data))
self.config_db.subscribe('LOOPBACK_INTERFACE', lambda table, key, data: self.lpbk_handler(key, data))
self.config_db.subscribe('FEATURE', lambda table, key, data: self.feature_state_handler(key, data))
if self.is_multi_npu:
self.config_db.subscribe('BGP_MONITORS', lambda table, key, data: self.masic_bgp_mon_cfg_handler(key, data))
syslog.syslog(syslog.LOG_INFO,
"Waiting for systemctl to finish initialization")
self.wait_till_system_init_done()
syslog.syslog(syslog.LOG_INFO,
"systemctl has finished initialization -- proceeding ...")
# Update all feature states once upon starting
self.update_all_feature_states()
# Defer load until subscribe
self.load()
self.config_db.listen()
def main():
daemon = HostConfigDaemon()
daemon.start()
if __name__ == "__main__":
main()