From f9af7aee3468786d941ac38ebdd4c9654f559ef6 Mon Sep 17 00:00:00 2001 From: Yevhen Fastiuk Date: Mon, 22 Aug 2022 00:47:53 +0300 Subject: [PATCH] [CLI] Move hostname, mgmt interface/vrf config to hostcfgd (#2) This PR depends on https://github.com/Azure/sonic-utilities/pull/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: 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: 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: 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](https://github.com/Azure/sonic-utilities/pull/2173) | ![GitHub issue/pull request detail](https://img.shields.io/github/pulls/detail/state/Azure/sonic-utilities/2173) | --- scripts/hostcfgd | 173 ++++++++++++++++++++++++++++++-- tests/hostcfgd/hostcfgd_test.py | 75 ++++++++++++++ tests/hostcfgd/test_vectors.py | 47 ++++++++- 3 files changed, 283 insertions(+), 12 deletions(-) diff --git a/scripts/hostcfgd b/scripts/hostcfgd index a82a630bfc0a..30ee0a28bd52 100755 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -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" @@ -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 @@ -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 @@ -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'] @@ -1306,6 +1449,9 @@ 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) @@ -1313,14 +1459,11 @@ class HostConfigDaemon: 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: @@ -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) @@ -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 @@ -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() diff --git a/tests/hostcfgd/hostcfgd_test.py b/tests/hostcfgd/hostcfgd_test.py index 786bd1c8f2a9..eae97368a0e2 100644 --- a/tests/hostcfgd/hostcfgd_test.py +++ b/tests/hostcfgd/hostcfgd_test.py @@ -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 @@ -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) diff --git a/tests/hostcfgd/test_vectors.py b/tests/hostcfgd/test_vectors.py index 43754252c0e3..c9c47a35af07 100644 --- a/tests/hostcfgd/test_vectors.py +++ b/tests/hostcfgd/test_vectors.py @@ -19,7 +19,7 @@ "enabled": "false", "num_dumps": "3", "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } + } }, "FEATURE": { "dhcp_relay": { @@ -118,7 +118,7 @@ "enabled": "false", "num_dumps": "3", "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } + } }, "FEATURE": { "dhcp_relay": { @@ -235,7 +235,7 @@ "enabled": "false", "num_dumps": "3", "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } + } }, "FEATURE": { "dhcp_relay": { @@ -331,7 +331,7 @@ "enabled": "false", "num_dumps": "3", "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } + } }, "FEATURE": { "dhcp_relay": { @@ -431,7 +431,7 @@ "enabled": "false", "num_dumps": "3", "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } + } }, "FEATURE": { "dhcp_relay": { @@ -507,6 +507,28 @@ ] ] +HOSTCFG_DAEMON_INIT_CFG_DB = { + "FEATURE": {}, + "AAA": {}, + "TACPLUS": {}, + "TACPLUS_SERVER": {}, + "RADIUS": {}, + "RADIUS_SERVER": {}, + "PASSW_HARDENING": {}, + "KDUMP": {}, + "NTP": {}, + "NTP_SERVER": {}, + "LOOPBACK_INTERFACE": {}, + "DEVICE_METADATA": { + "localhost": { + "hostname": "old-hostname" + } + }, + "MGMT_INTERFACE": {}, + "MGMT_VRF_CONFIG": {} +} + + HOSTCFG_DAEMON_CFG_DB = { "FEATURE": { "dhcp_relay": { @@ -538,6 +560,12 @@ "status": "enabled" }, }, + "AAA": {}, + "TACPLUS": {}, + "TACPLUS_SERVER": {}, + "RADIUS": {}, + "RADIUS_SERVER": {}, + "PASSW_HARDENING": {}, "KDUMP": { "config": { @@ -562,6 +590,15 @@ "localhost": { "subtype": "DualToR", "type": "ToRRouter", + "hostname": "SomeNewHostname" + } + }, + "MGMT_INTERFACE": { + "eth0|1.2.3.4/24": {} + }, + "MGMT_VRF_CONFIG": { + "vrf_global": { + 'mgmtVrfEnabled': 'true' } } }