Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support host side rate limit configuration #22

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 87 additions & 5 deletions scripts/hostcfgd
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ class FeatureHandler(object):
if not feature_name:
syslog.syslog(syslog.LOG_WARNING, "Feature is None")
continue

device_config = {}
device_config.update(self._device_config)
device_config.update(self._device_running_config)
Expand Down Expand Up @@ -315,7 +315,7 @@ class FeatureHandler(object):
def sync_feature_asic_scope(self, feature_config):
"""Updates the has_per_asic_scope field in the FEATURE|* tables as the field
might have to be rendered based on DEVICE_METADATA table or Device Running configuration.
Disable the ASIC instance service unit file it the render value is False and update config
Disable the ASIC instance service unit file it the render value is False and update config

Args:
feature: An object represents a feature's configuration in `FEATURE`
Expand Down Expand Up @@ -960,7 +960,7 @@ class PasswHardening(object):
def __init__(self):
self.passw_policies_default = {}
self.passw_policies = {}

self.debug = False
self.trace = False

Expand Down Expand Up @@ -1134,7 +1134,7 @@ class PasswHardening(object):
def modify_passw_conf_file(self):
passw_policies = self.passw_policies_default.copy()
passw_policies.update(self.passw_policies)

# set new Password Hardening policies.
self.set_passw_hardening_policies(passw_policies)

Expand Down Expand Up @@ -1328,6 +1328,7 @@ class PamLimitsCfg(object):
"modify pam_limits config file failed with exception: {}"
.format(e))


class DeviceMetaCfg(object):
"""
DeviceMetaCfg Config Daemon
Expand Down Expand Up @@ -1465,6 +1466,77 @@ class MgmtIfaceCfg(object):
self.mgmt_vrf_enabled = enabled


class SyslogCfg:
SYSLOG_RATE_LIMIT_INTERVAL = 'rate_limit_interval'
SYSLOG_RATE_LIMIT_BURST = 'rate_limit_burst'
HOST_KEY = 'GLOBAL'

# syslog conf file path in docker
SYSLOG_CONF_PATH = '/etc/rsyslog.conf'
Junchao-Mellanox marked this conversation as resolved.
Show resolved Hide resolved

# Regular expressions to extract value from rsyslog.conf
INTERVAL_PATTERN = '.*SystemLogRateLimitInterval\s+(\d+).*'
BURST_PATTERN = '.*SystemLogRateLimitBurst\s+(\d+).*'

def __init__(self):
self.current_interval, self.current_burst = self.parse_syslog_conf()

def syslog_update(self, data):
"""Update syslog related configuration

Args:
data (dict): CONFIG DB data: {<field_name>: <field_value>}
"""
new_interval = '0'
saiarcot895 marked this conversation as resolved.
Show resolved Hide resolved
new_burst = '0'
if data:
new_interval = data.get(self.SYSLOG_RATE_LIMIT_INTERVAL, '0')
Junchao-Mellanox marked this conversation as resolved.
Show resolved Hide resolved
new_burst = data.get(self.SYSLOG_RATE_LIMIT_BURST, '0')

if new_interval == self.current_interval and new_burst == self.current_burst:
return

syslog.syslog(syslog.LOG_INFO, f'Configure syslog rate limit interval={new_interval} (old:{self.current_interval}), burst={new_burst} (old:{self.current_burst})')

try:
run_cmd('systemctl reset-failed rsyslog-config rsyslog', raise_exception=True)
run_cmd('systemctl restart rsyslog-config', raise_exception=True)
self.current_interval = new_interval
self.current_burst = new_burst
except Exception as e:
syslog.syslog(syslog.LOG_ERR, f'Failed to configure syslog rate limit for host - {e}')

def load(self, data):
if self.HOST_KEY in data:
self.syslog_update(data[self.HOST_KEY])

def parse_syslog_conf(self):
"""Parse existing syslog conf and extract config values

Returns:
tuple: interval,burst,target_ip
"""
interval = '0'
Junchao-Mellanox marked this conversation as resolved.
Show resolved Hide resolved
burst = '0'

try:
with open(self.SYSLOG_CONF_PATH, 'r') as f:
content = f.read()
pattern = re.compile(self.INTERVAL_PATTERN)
for match in pattern.finditer(content):
interval = match.group(1)
break

pattern = re.compile(self.BURST_PATTERN)
for match in pattern.finditer(content):
burst = match.group(1)
break
except OSError:
syslog.syslog(syslog.LOG_ERR, f'Failed to read file {self.SYSLOG_CONF_PATH}')
return interval, burst
return interval, burst


class HostConfigDaemon:
def __init__(self):
# Just a sanity check to verify if the CONFIG_DB has been initialized
Expand Down Expand Up @@ -1511,6 +1583,9 @@ class HostConfigDaemon:
# Initialize MgmtIfaceCfg
self.mgmtifacecfg = MgmtIfaceCfg()

# Initialize SyslogCfg
self.syslogcfg = SyslogCfg()

def load(self, init_data):
features = init_data['FEATURE']
aaa = init_data['AAA']
Expand All @@ -1526,6 +1601,7 @@ class HostConfigDaemon:
dev_meta = init_data.get(swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, {})
mgmt_ifc = init_data.get(swsscommon.CFG_MGMT_INTERFACE_TABLE_NAME, {})
mgmt_vrf = init_data.get(swsscommon.CFG_MGMT_VRF_CONFIG_TABLE_NAME, {})
syslog = init_data.get('SYSLOG_CONFIG', {})

self.feature_handler.sync_state_field(features)
self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server)
Expand All @@ -1535,6 +1611,7 @@ class HostConfigDaemon:
self.passwcfg.load(passwh)
self.devmetacfg.load(dev_meta)
self.mgmtifacecfg.load(mgmt_ifc, mgmt_vrf)
self.syslogcfg.load(syslog)

# Update AAA with the hostname
self.aaacfg.hostname_update(self.devmetacfg.hostname)
Expand Down Expand Up @@ -1634,6 +1711,9 @@ class HostConfigDaemon:
syslog.syslog(syslog.LOG_INFO, 'DeviceMeta handler...')
self.devmetacfg.hostname_update(data)

def syslog_handler(self, key, op, data):
self.syslogcfg.syslog_update(data)

def wait_till_system_init_done(self):
# No need to print the output in the log file so using the "--quiet"
# flag
Expand Down Expand Up @@ -1672,7 +1752,7 @@ class HostConfigDaemon:
self.config_db.subscribe('VLAN_SUB_INTERFACE', make_callback(self.vlan_sub_intf_handler))
self.config_db.subscribe('PORTCHANNEL_INTERFACE', make_callback(self.portchannel_intf_handler))
self.config_db.subscribe('INTERFACE', make_callback(self.phy_intf_handler))

# Handle DEVICE_MEATADATA changes
self.config_db.subscribe(swsscommon.CFG_DEVICE_METADATA_TABLE_NAME,
make_callback(self.device_metadata_handler))
Expand All @@ -1681,6 +1761,8 @@ class HostConfigDaemon:
self.config_db.subscribe(swsscommon.CFG_MGMT_VRF_CONFIG_TABLE_NAME,
make_callback(self.mgmt_vrf_handler))

self.config_db.subscribe('SYSLOG_CONFIG', make_callback(self.syslog_handler))

syslog.syslog(syslog.LOG_INFO,
"Waiting for systemctl to finish initialization")
self.wait_till_system_init_done()
Expand Down
60 changes: 58 additions & 2 deletions tests/hostcfgd/hostcfgd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
hostcfgd.Table = mock.Mock()

class TestFeatureHandler(TestCase):
"""Test methods of `FeatureHandler` class.
"""Test methods of `FeatureHandler` class.
"""
def checks_config_table(self, feature_table, expected_table):
"""Compares `FEATURE` table in `CONFIG_DB` with expected output table.
Expand Down Expand Up @@ -423,7 +423,7 @@ def test_devicemeta_event(self):
def test_mgmtiface_event(self):
"""
Test handling mgmt events.
1) Management interface setup
1) Management interface setup
2) Management vrf setup
"""
MockConfigDb.set_config_db(HOSTCFG_DAEMON_CFG_DB)
Expand Down Expand Up @@ -461,3 +461,59 @@ def test_mgmtiface_event(self):
]
mocked_subprocess.check_call.assert_has_calls(expected,
any_order=True)

class TestSyslogHandler:
@mock.patch('hostcfgd.run_cmd')
@mock.patch('hostcfgd.SyslogCfg.parse_syslog_conf', mock.MagicMock(return_value=('100', '200')))
def test_syslog_update(self, mock_run_cmd):
syslog_cfg = hostcfgd.SyslogCfg()
data = {
'rate_limit_interval': '100',
'rate_limit_burst': '200'
}
syslog_cfg.syslog_update(data)
mock_run_cmd.assert_not_called()

data = {
'rate_limit_interval': '200',
'rate_limit_burst': '200'
}
syslog_cfg.syslog_update(data)
expected = [call('systemctl reset-failed rsyslog-config rsyslog', raise_exception=True),
call('systemctl restart rsyslog-config', raise_exception=True)]
mock_run_cmd.assert_has_calls(expected)

data = {
'rate_limit_interval': '100',
'rate_limit_burst': '100'
}
mock_run_cmd.side_effect = Exception()
syslog_cfg.syslog_update(data)
# when exception occurs, interval and burst should not be updated
assert syslog_cfg.current_interval == '200'
assert syslog_cfg.current_burst == '200'

def test_load(self):
syslog_cfg = hostcfgd.SyslogCfg()
syslog_cfg.syslog_update = mock.MagicMock()

data = {}
syslog_cfg.load(data)
syslog_cfg.syslog_update.assert_not_called()

data = {syslog_cfg.HOST_KEY: {}}
syslog_cfg.load(data)
syslog_cfg.syslog_update.assert_called_once()

def test_parse_syslog_conf(self):
syslog_cfg = hostcfgd.SyslogCfg()

syslog_cfg.SYSLOG_CONF_PATH = os.path.join(test_path, 'hostcfgd', 'mock_rsyslog.conf')
interval, burst = syslog_cfg.parse_syslog_conf()
assert interval == '50'
assert burst == '10002'

syslog_cfg.SYSLOG_CONF_PATH = os.path.join(test_path, 'hostcfgd', 'mock_empty_rsyslog.conf')
interval, burst = syslog_cfg.parse_syslog_conf()
assert interval == '0'
assert burst == '0'
Empty file.
29 changes: 29 additions & 0 deletions tests/hostcfgd/mock_rsyslog.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
$ModLoad imuxsock # provides support for local system logging

#
# Set a rate limit on messages from the container
#


$SystemLogRateLimitInterval 50
$SystemLogRateLimitBurst 10002

#$ModLoad imklog # provides kernel logging support
#$ModLoad immark # provides --MARK-- message capability

# provides UDP syslog reception
#$ModLoad imudp
#$UDPServerRun 514

# provides TCP syslog reception
#$ModLoad imtcp
#$InputTCPServerRun 514


###########################
#### GLOBAL DIRECTIVES ####
###########################

# Set remote syslog server
template (name="ForwardFormatInContainer" type="string" string="<%PRI%>%TIMESTAMP:::date-rfc3339% %HOSTNAME% pmon#%syslogtag%%msg:::sp-if-no-1st-sp%%msg%")
*.* action(type="omfwd" target="127.0.0.1" port="514" protocol="udp" Template="ForwardFormatInContainer")