Skip to content

Commit

Permalink
[CLI] Move hostname, mgmt interface/vrf config to hostcfgd (zhenggen-…
Browse files Browse the repository at this point in the history
…xu#2)

This PR depends on sonic-net/sonic-utilities#2173

#### Why I did it
To be able to configure the management interface and hostname standalone by changing database config at runtime.
From the CLI perspective fo view, the following behavior is the same. But now you have two ways of configuring it: CLI, directly through the database.

#### How I did it
Moved configuration part of the interface and hostname to "hostcfgd".

#### How to verify it
* Built an image
* Flash it to the switch
* Run CLI commands
```
# Set IP address: verify address is set on the iface
sudo config interface ip add eth0 10.210.25.127/22 10.210.24.1
ip address show eth0
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
#     link/ether 98:03:9b:a2:be:80 brd ff:ff:ff:ff:ff:ff
#     inet 10.210.25.127/22 brd 10.210.27.255 scope global eth0
#        valid_lft forever preferred_lft forever
#     inet6 fe80::9a03:9bff:fea2:be80/64 scope link
#       valid_lft forever preferred_lft forever

# Remove IP address: verify you received address form DHCP
sudo config interface ip remove eth0 10.210.25.127/22
ip address show eth0
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
#     link/ether 98:03:9b:a2:be:80 brd ff:ff:ff:ff:ff:ff
#     inet 10.210.25.127/22 brd 10.210.27.255 scope global eth0
#        valid_lft forever preferred_lft forever
#     inet6 fe80::9a03:9bff:fea2:be80/64 scope link
#       valid_lft forever preferred_lft forever

# Enable/disable mgmt VRF
ip address show mgmt
# Device "mgmt" does not exist.

sudo config vrf add mgmt
ip address show mgmt
# 72: mgmt: <NOARP,MASTER,UP,LOWER_UP> mtu 65575 qdisc noqueue state UP group default qlen 1000
#     link/ether fa:9b:ad:7b:1e:83 brd ff:ff:ff:ff:ff:ff

sudo config vrf del mgmt
ip address show mgmt
# Device "mgmt" does not exist.

# Setting the hostname
admin@r-anaconda-27:~$ sudo config hostname bla
# Login / Logout
admin@bla:~$
```
#### Description for the changelog
* Moved management interface configuration to hostcfgd.
* Moved management VRF configuration to hostcfgd.
* Moved hostname configuration to hostcfgd.

#### Submodules PR's :

| Repo  | PR title | State |
| ----------------- | ----------------- | ----------------- |
| sonic-utilities | [[CLI] Move hostname, mgmt interface/vrf config to hostcfgd](sonic-net/sonic-utilities#2173) | ![GitHub issue/pull request detail](https://img.shields.io/github/pulls/detail/state/Azure/sonic-utilities/2173) |
  • Loading branch information
fastiuk authored Aug 21, 2022
1 parent 70ce6a3 commit f9af7ae
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 12 deletions.
173 changes: 166 additions & 7 deletions scripts/hostcfgd
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import re
import jinja2
from sonic_py_common import device_info
from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table
from swsscommon import swsscommon

# FILE
PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
Expand Down Expand Up @@ -1253,6 +1254,143 @@ class PamLimitsCfg(object):
"modify pam_limits config file failed with exception: {}"
.format(e))

class DeviceMetaCfg(object):
"""
DeviceMetaCfg Config Daemon
Handles changes in DEVICE_METADATA table.
1) Handle hostname change
"""

def __init__(self):
self.hostname = ''

def load(self, dev_meta={}):
# Get hostname initial
self.hostname = dev_meta.get('localhost', {}).get('hostname', '')
syslog.syslog(syslog.LOG_DEBUG, f'Initial hostname: {self.hostname}')

def hostname_update(self, data):
"""
Apply hostname handler.
Args:
data: Read table's key's data.
"""
syslog.syslog(syslog.LOG_DEBUG, 'DeviceMetaCfg: hostname update')
new_hostname = data.get('hostname')

# Restart hostname-config service when hostname was changed.
# Empty not allowed
if new_hostname and new_hostname != self.hostname:
syslog.syslog(syslog.LOG_INFO, 'DeviceMetaCfg: Set new hostname: {}'
.format(new_hostname))
self.hostname = new_hostname
try:
run_cmd('sudo service hostname-config restart', True, True)
except subprocess.CalledProcessError as e:
syslog.syslog(syslog.LOG_ERR, 'DeviceMetaCfg: Failed to set new'
' hostname: {}'.format(e))
return

run_cmd('sudo monit reload')
else:
msg = 'Hostname was not updated: '
msg += 'Already set up' if new_hostname else 'Empty not allowed'
syslog.syslog(syslog.LOG_ERR, msg)


class MgmtIfaceCfg(object):
"""
MgmtIfaceCfg Config Daemon
Handles changes in MGMT_INTERFACE, MGMT_VRF_CONFIG tables.
1) Handle change of interface ip
2) Handle change of management VRF state
"""

def __init__(self):
self.iface_config_data = {}
self.mgmt_vrf_enabled = ''

def load(self, mgmt_iface={}, mgmt_vrf={}):
# Get initial data
self.iface_config_data = mgmt_iface
self.mgmt_vrf_enabled = mgmt_vrf.get('mgmtVrfEnabled', '')
syslog.syslog(syslog.LOG_DEBUG,
f'Initial mgmt interface conf: {self.iface_config_data}')
syslog.syslog(syslog.LOG_DEBUG,
f'Initial mgmt VRF state: {self.mgmt_vrf_enabled}')

def update_mgmt_iface(self, iface, key, data):
"""Handle update management interface config
"""
syslog.syslog(syslog.LOG_DEBUG, 'MgmtIfaceCfg: mgmt iface update')

# Restart management interface service when config was changed
if data != self.iface_config_data.get(key):
cfg = {key: data}
syslog.syslog(syslog.LOG_INFO, f'MgmtIfaceCfg: Set new interface '
f'config {cfg} for {iface}')
try:
run_cmd('sudo systemctl restart interfaces-config', True, True)
run_cmd('sudo systemctl restart ntp-config', True, True)
except subprocess.CalledProcessError:
syslog.syslog(syslog.LOG_ERR, f'Failed to restart management '
'interface services')
return

self.iface_config_data[key] = data

def update_mgmt_vrf(self, data):
"""Handle update management VRF state
"""
syslog.syslog(syslog.LOG_DEBUG, 'MgmtIfaceCfg: mgmt vrf state update')

# Restart mgmt vrf services when mgmt vrf config was changed.
# Empty not allowed.
enabled = data.get('mgmtVrfEnabled', '')
if not enabled or enabled == self.mgmt_vrf_enabled:
return

syslog.syslog(syslog.LOG_INFO, f'Set mgmt vrf state {enabled}')

# Restart related vrfs services
try:
run_cmd('service ntp stop', True, True)
run_cmd('systemctl restart interfaces-config', True, True)
run_cmd('service ntp start', True, True)
except subprocess.CalledProcessError:
syslog.syslog(syslog.LOG_ERR, f'Failed to restart management vrf '
'services')
return

# Remove mgmt if route
if enabled == 'true':
"""
The regular expression for grep in below cmd is to match eth0 line
in /proc/net/route, sample file:
$ cat /proc/net/route
Iface Destination Gateway Flags RefCnt Use
eth0 00000000 01803B0A 0003 0 0
#################### Line break here ####################
Metric Mask MTU Window IRTT
202 00000000 0 0 0
"""
try:
run_cmd(r"""cat /proc/net/route | grep -E \"eth0\s+"""
r"""00000000\s+[0-9A-Z]+\s+[0-9]+\s+[0-9]+\s+[0-9]+"""
r"""\s+202\" | wc -l""",
True, True)
except subprocess.CalledProcessError:
syslog.syslog(syslog.LOG_ERR, 'MgmtIfaceCfg: Could not delete '
'eth0 route')
return

run_cmd("ip -4 route del default dev eth0 metric 202", False)

# Update cache
self.mgmt_vrf_enabled = enabled


class HostConfigDaemon:
def __init__(self):
# Just a sanity check to verify if the CONFIG_DB has been initialized
Expand Down Expand Up @@ -1284,7 +1422,6 @@ class HostConfigDaemon:
self.is_multi_npu = device_info.is_multi_npu()

# Initialize AAACfg
self.hostname_cache=""
self.aaacfg = AaaCfg()

# Initialize PasswHardening
Expand All @@ -1294,6 +1431,12 @@ class HostConfigDaemon:
self.pamLimitsCfg = PamLimitsCfg(self.config_db)
self.pamLimitsCfg.update_config_file()

# Initialize DeviceMetaCfg
self.devmetacfg = DeviceMetaCfg()

# Initialize MgmtIfaceCfg
self.mgmtifacecfg = MgmtIfaceCfg()

def load(self, init_data):
features = init_data['FEATURE']
aaa = init_data['AAA']
Expand All @@ -1306,21 +1449,21 @@ class HostConfigDaemon:
ntp_global = init_data['NTP']
kdump = init_data['KDUMP']
passwh = init_data['PASSW_HARDENING']
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, {})

self.feature_handler.sync_state_field(features)
self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server)
self.iptables.load(lpbk_table)
self.ntpcfg.load(ntp_global, ntp_server)
self.kdumpCfg.load(kdump)
self.passwcfg.load(passwh)

dev_meta = self.config_db.get_table('DEVICE_METADATA')
if 'localhost' in dev_meta:
if 'hostname' in dev_meta['localhost']:
self.hostname_cache = dev_meta['localhost']['hostname']
self.devmetacfg.load(dev_meta)
self.mgmtifacecfg.load(mgmt_ifc, mgmt_vrf)

# Update AAA with the hostname
self.aaacfg.hostname_update(self.hostname_cache)
self.aaacfg.hostname_update(self.devmetacfg.hostname)

def __get_intf_name(self, key):
if isinstance(key, tuple) and key:
Expand Down Expand Up @@ -1370,6 +1513,10 @@ class HostConfigDaemon:
mgmt_intf_name = self.__get_intf_name(key)
self.aaacfg.handle_radius_source_intf_ip_chg(mgmt_intf_name)
self.aaacfg.handle_radius_nas_ip_chg(mgmt_intf_name)
self.mgmtifacecfg.update_mgmt_iface(mgmt_intf_name, key, data)

def mgmt_vrf_handler(self, key, op, data):
self.mgmtifacecfg.update_mgmt_vrf(data)

def lpbk_handler(self, key, op, data):
key = ConfigDBConnector.deserialize_key(key)
Expand Down Expand Up @@ -1409,6 +1556,10 @@ class HostConfigDaemon:
syslog.syslog(syslog.LOG_INFO, 'Kdump handler...')
self.kdumpCfg.kdump_update(key, data)

def device_metadata_handler(self, key, op, data):
syslog.syslog(syslog.LOG_INFO, 'DeviceMeta handler...')
self.devmetacfg.hostname_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 @@ -1448,6 +1599,14 @@ class HostConfigDaemon:
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))

# Handle MGMT_VRF_CONFIG changes
self.config_db.subscribe(swsscommon.CFG_MGMT_VRF_CONFIG_TABLE_NAME,
make_callback(self.mgmt_vrf_handler))

syslog.syslog(syslog.LOG_INFO,
"Waiting for systemctl to finish initialization")
self.wait_till_system_init_done()
Expand Down
75 changes: 75 additions & 0 deletions tests/hostcfgd/hostcfgd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sonic_py_common.general import load_module_from_source
from unittest import TestCase, mock

from .test_vectors import HOSTCFG_DAEMON_INIT_CFG_DB
from .test_vectors import HOSTCFGD_TEST_VECTOR, HOSTCFG_DAEMON_CFG_DB
from tests.common.mock_configdb import MockConfigDb, MockDBConnector

Expand Down Expand Up @@ -357,3 +358,77 @@ def test_kdump_event(self):
call('sonic-kdump-config --num_dumps 3', shell=True),
call('sonic-kdump-config --memory 0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M', shell=True)]
mocked_subprocess.check_call.assert_has_calls(expected, any_order=True)

def test_devicemeta_event(self):
"""
Test handling DEVICE_METADATA events.
1) Hostname reload
"""
MockConfigDb.set_config_db(HOSTCFG_DAEMON_CFG_DB)
MockConfigDb.event_queue = [(swsscommon.CFG_DEVICE_METADATA_TABLE_NAME,
'localhost')]
daemon = hostcfgd.HostConfigDaemon()
daemon.aaacfg = mock.MagicMock()
daemon.iptables = mock.MagicMock()
daemon.passwcfg = mock.MagicMock()
daemon.load(HOSTCFG_DAEMON_INIT_CFG_DB)
daemon.register_callbacks()
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
popen_mock = mock.Mock()
attrs = {'communicate.return_value': ('output', 'error')}
popen_mock.configure_mock(**attrs)
mocked_subprocess.Popen.return_value = popen_mock

try:
daemon.start()
except TimeoutError:
pass

expected = [
call('sudo service hostname-config restart', shell=True),
call('sudo monit reload', shell=True)
]
mocked_subprocess.check_call.assert_has_calls(expected,
any_order=True)

def test_mgmtiface_event(self):
"""
Test handling mgmt events.
1) Management interface setup
2) Management vrf setup
"""
MockConfigDb.set_config_db(HOSTCFG_DAEMON_CFG_DB)
MockConfigDb.event_queue = [
(swsscommon.CFG_MGMT_INTERFACE_TABLE_NAME, 'eth0|1.2.3.4/24'),
(swsscommon.CFG_MGMT_VRF_CONFIG_TABLE_NAME, 'vrf_global')
]
daemon = hostcfgd.HostConfigDaemon()
daemon.register_callbacks()
daemon.aaacfg = mock.MagicMock()
daemon.iptables = mock.MagicMock()
daemon.passwcfg = mock.MagicMock()
daemon.load(HOSTCFG_DAEMON_INIT_CFG_DB)
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
popen_mock = mock.Mock()
attrs = {'communicate.return_value': ('output', 'error')}
popen_mock.configure_mock(**attrs)
mocked_subprocess.Popen.return_value = popen_mock

try:
daemon.start()
except TimeoutError:
pass

expected = [
call('sudo systemctl restart interfaces-config', shell=True),
call('sudo systemctl restart ntp-config', shell=True),
call('service ntp stop', shell=True),
call('systemctl restart interfaces-config', shell=True),
call('service ntp start', shell=True),
call('cat /proc/net/route | grep -E \\"eth0\\s+00000000'
'\\s+[0-9A-Z]+\\s+[0-9]+\\s+[0-9]+\\s+[0-9]+\\s+202\\" | '
'wc -l', shell=True),
call('ip -4 route del default dev eth0 metric 202', shell=True)
]
mocked_subprocess.check_call.assert_has_calls(expected,
any_order=True)
Loading

0 comments on commit f9af7ae

Please sign in to comment.