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

New host service to support gnmi full config update #178

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ stages:
sudo dpkg -i libnl-route-3-200_*.deb
sudo dpkg -i libnl-nf-3-200_*.deb
sudo dpkg -i libyang_1.0.73_*.deb
sudo dpkg -i libyang-cpp_1.0.73_*.deb
sudo dpkg -i python3-yang_1.0.73_*.deb
sudo dpkg -i libswsscommon_1.0.0_amd64.deb
sudo dpkg -i python3-swsscommon_1.0.0_amd64.deb
workingDirectory: $(Pipeline.Workspace)/target/debs/bullseye/
Expand Down
41 changes: 38 additions & 3 deletions host_modules/config_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,52 @@ class Config(host_service.HostModule):
"""
DBus endpoint that executes the config command
"""
@host_service.method(host_service.bus_name(MOD_NAME), in_signature='s', out_signature='is')
def reload(self, config_db_json):
@host_service.method(host_service.bus_name(MOD_NAME), in_signature='ss', out_signature='is')
def reload(self, config_db_json, caller):

if caller and len(caller.strip()):
# Mask the caller service, should not restart after config reload
cmd = ['/usr/bin/systemctl', 'mask', caller]
subprocess.run(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
cmd = ['/usr/local/bin/config', 'reload', '-y']
if config_db_json and len(config_db_json.strip()):
cmd.append('/dev/stdin')
input_bytes = (config_db_json + '\n').encode('utf-8')
result = subprocess.run(cmd, input=input_bytes, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
else:
result = subprocess.run(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if caller and len(caller.strip()):
# Unmask the caller service
cmd = ['/usr/bin/systemctl', 'unmask', caller]
subprocess.run(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
msg = ''
if result.returncode:
lines = result.stderr.decode().split('\n')
for line in lines:
if 'Error' in line:
msg = line
break
return result.returncode, msg

@host_service.method(host_service.bus_name(MOD_NAME), in_signature='ss', out_signature='is')
def reload_force(self, config_db_json, caller):

if caller and len(caller.strip()):
# Mask the caller service, should not restart after config reload
cmd = ['/usr/bin/systemctl', 'mask', caller]
subprocess.run(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Force config reload without system checks
cmd = ['/usr/local/bin/config', 'reload', '-y', '-f']
if config_db_json and len(config_db_json.strip()):
cmd.append('/dev/stdin')
input_bytes = (config_db_json + '\n').encode('utf-8')
result = subprocess.run(cmd, input=input_bytes, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
else:
result = subprocess.run(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if caller and len(caller.strip()):
# Unmask the caller service
cmd = ['/usr/bin/systemctl', 'unmask', caller]
subprocess.run(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
msg = ''
if result.returncode:
lines = result.stderr.decode().split('\n')
Expand All @@ -45,4 +81,3 @@ def save(self, config_file):
msg = line
break
return result.returncode, msg

27 changes: 27 additions & 0 deletions host_modules/yang_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Yang validation handler"""

from host_modules import host_service
import json
import sonic_yang

YANG_MODELS_DIR = "/usr/local/yang-models"
MOD_NAME = 'yang'

class Yang(host_service.HostModule):
"""
DBus endpoint that runs yang validation
"""
@host_service.method(host_service.bus_name(MOD_NAME), in_signature='s', out_signature='is')
def validate(self, config_db_json):
config = json.loads(config_db_json)
# Run yang validation
yang_parser = sonic_yang.SonicYang(YANG_MODELS_DIR)
yang_parser.loadYangModel()
try:
yang_parser.loadData(configdbJson=config)
yang_parser.validate_data_tree()
except sonic_yang.SonicYangException as e:
return -1, str(e)
if len(yang_parser.tablesWithOutYang):
return -1, "Tables without yang models: " + str(yang_parser.tablesWithOutYang)
return 0, ""
5 changes: 3 additions & 2 deletions scripts/sonic-host-server
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import dbus.service
import dbus.mainloop.glib

from gi.repository import GObject
from host_modules import config_engine, gcu, host_service, showtech, systemd_service, file_service
from host_modules import config_engine, gcu, host_service, showtech, systemd_service, file_service, yang_validator


def register_dbus():
Expand All @@ -23,7 +23,8 @@ def register_dbus():
'host_service': host_service.HostService('host_service'),
'showtech': showtech.Showtech('showtech'),
'systemd': systemd_service.SystemdService('systemd'),
'file_stat': file_service.FileService('file')
'file_stat': file_service.FileService('file'),
'yang': yang_validator.Yang('yang')
}
for mod_name, handler_class in mod_dict.items():
handlers[mod_name] = handler_class
Expand Down
67 changes: 59 additions & 8 deletions tests/host_modules/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ def test_reload(self, MockInit, MockBusName, MockSystemBus):
mock_run.return_value = res_mock
config_db_json = "{}"
config_stub = config_engine.Config(config_engine.MOD_NAME)
ret, msg = config_stub.reload(config_db_json)
call_args = mock_run.call_args[0][0]
assert "reload" in call_args
assert "/dev/stdin" in call_args
ret, msg = config_stub.reload(config_db_json, "gnmi")
call_args_list = mock_run.call_args_list
args, _ = call_args_list[0]
assert ["/usr/bin/systemctl", "mask", "gnmi"] in args
args, _ = call_args_list[1]
assert ["/usr/local/bin/config", "reload", "-y", "/dev/stdin"] in args
args, _ = call_args_list[2]
assert ["/usr/bin/systemctl", "unmask", "gnmi"] in args
assert ret == test_ret, "Return value is wrong"
assert msg == "", "Return message is wrong"
with mock.patch("subprocess.run") as mock_run:
Expand All @@ -33,10 +37,57 @@ def test_reload(self, MockInit, MockBusName, MockSystemBus):
mock_run.return_value = res_mock
config_db_json = "{}"
config_stub = config_engine.Config(config_engine.MOD_NAME)
ret, msg = config_stub.reload(config_db_json)
call_args = mock_run.call_args[0][0]
assert "reload" in call_args
assert "/dev/stdin" in call_args
ret, msg = config_stub.reload(config_db_json, "gnmi")
call_args_list = mock_run.call_args_list
args, _ = call_args_list[0]
assert ["/usr/bin/systemctl", "mask", "gnmi"] in args
args, _ = call_args_list[1]
assert ["/usr/local/bin/config", "reload", "-y", "/dev/stdin"] in args
args, _ = call_args_list[2]
assert ["/usr/bin/systemctl", "unmask", "gnmi"] in args
assert ret == test_ret, "Return value is wrong"
assert msg == "Error: this is the test message", "Return message is wrong"

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_reload_force(self, MockInit, MockBusName, MockSystemBus):
with mock.patch("subprocess.run") as mock_run:
res_mock = mock.Mock()
test_ret = 0
test_msg = b"Error: this is the test message\nHello world\n"
attrs = {"returncode": test_ret, "stderr": test_msg}
res_mock.configure_mock(**attrs)
mock_run.return_value = res_mock
config_db_json = "{}"
config_stub = config_engine.Config(config_engine.MOD_NAME)
ret, msg = config_stub.reload_force(config_db_json, "gnmi")
call_args_list = mock_run.call_args_list
args, _ = call_args_list[0]
assert ["/usr/bin/systemctl", "mask", "gnmi"] in args
args, _ = call_args_list[1]
assert ["/usr/local/bin/config", "reload", "-y", "-f", "/dev/stdin"] in args
args, _ = call_args_list[2]
assert ["/usr/bin/systemctl", "unmask", "gnmi"] in args
assert ret == test_ret, "Return value is wrong"
assert msg == "", "Return message is wrong"
with mock.patch("subprocess.run") as mock_run:
res_mock = mock.Mock()
test_ret = 1
test_msg = b"Error: this is the test message\nHello world\n"
attrs = {"returncode": test_ret, "stderr": test_msg}
res_mock.configure_mock(**attrs)
mock_run.return_value = res_mock
config_db_json = "{}"
config_stub = config_engine.Config(config_engine.MOD_NAME)
ret, msg = config_stub.reload_force(config_db_json, "gnmi")
call_args_list = mock_run.call_args_list
args, _ = call_args_list[0]
assert ["/usr/bin/systemctl", "mask", "gnmi"] in args
args, _ = call_args_list[1]
assert ["/usr/local/bin/config", "reload", "-y", "-f", "/dev/stdin"] in args
args, _ = call_args_list[2]
assert ["/usr/bin/systemctl", "unmask", "gnmi"] in args
assert ret == test_ret, "Return value is wrong"
assert msg == "Error: this is the test message", "Return message is wrong"

Expand Down
15 changes: 15 additions & 0 deletions tests/host_modules/yang_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import sys
import os
import pytest
from unittest import mock
from host_modules import yang_validator

class TestConfigEngine(object):
@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_reload(self, MockInit, MockBusName, MockSystemBus):
config_db_json = "{}"
yang_stub = yang_validator.Yang(yang_validator.MOD_NAME)
ret, _ = yang_stub.validate(config_db_json)
assert ret == 0, "Yang validation failed"
Loading