Skip to content

Commit

Permalink
Merge branch 'master' into gnoi_wr_hs_new
Browse files Browse the repository at this point in the history
  • Loading branch information
rkavitha-hcl authored and Bibhuprasad Singh committed Feb 12, 2025
2 parents f568f97 + 9c49913 commit 61e547d
Show file tree
Hide file tree
Showing 12 changed files with 404 additions and 116 deletions.
36 changes: 35 additions & 1 deletion host_modules/docker_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import docker
import signal
import errno
import json
import logging

MOD_NAME = "docker_service"
Expand Down Expand Up @@ -273,4 +274,37 @@ def load(self, image):
except FileNotFoundError:
return errno.ENOENT, "File {} not found.".format(image)
except Exception as e:
return 1, "Failed to load image {}: {}".format(image, str(e))
return 1, "Failed to load image {}: {}".format(image, str(e))

@host_service.method(
host_service.bus_name(MOD_NAME), in_signature="ba{sv}", out_signature="is"
)
def list(self, all, filter):
"""
List Docker containers.
Args:
all (bool): Whether to list all containers or only running ones.
filter (dict): Filters to apply when listing containers.
Returns:
tuple: A tuple containing the exit code (int) and a JSON string of the container list.
"""
try:
client = docker.from_env()
listed_containers = client.containers.list(all=all, filters=filter)
container_list = [
{
"id": container.id,
"name": container.name,
"status": container.status,
"image": container.image.tags[0] if container.image.tags else "",
"labels": container.labels,
"hash": container.image.id,
}
for container in listed_containers
]
logging.info("List of containers: {}".format(container_list))
return 0, json.dumps(container_list)
except Exception as e:
return 1, "Failed to list containers: {} {}".format(str(e), container_list)
63 changes: 63 additions & 0 deletions host_modules/image_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import requests
import stat
import subprocess
import json

from host_modules import host_service
import tempfile
Expand Down Expand Up @@ -133,3 +134,65 @@ def checksum(self, file_path, algorithm):
except Exception as e:
logger.error("Failed to calculate checksum: {}".format(e))
return errno.EIO, str(e)

@host_service.method(
host_service.bus_name(MOD_NAME), in_signature="", out_signature="is"
)
def list_images(self):
"""
List the current, next, and available SONiC images.
Returns:
A tuple with an error code and a JSON string with keys "current", "next", and "available" or an error message.
"""
logger.info("Listing SONiC images")

try:
output = subprocess.check_output(
["/usr/local/bin/sonic-installer", "list"],
stderr=subprocess.STDOUT,
).decode().strip()
result = self._parse_sonic_installer_list(output)
logger.info("List result: {}".format(result))
return 0, json.dumps(result)
except subprocess.CalledProcessError as e:
msg = "Failed to list images: command {} failed with return code {} and message {}".format(e.cmd, e.returncode, e.output.decode())
logger.error(msg)
return e.returncode, msg

def _parse_sonic_installer_list(self, output):
"""
Parse the output of the sonic-installer list command.
Args:
output: The output of the sonic-installer list command.
Returns:
A dictionary with keys "current", "next", and "available" containing the respective images.
"""
current_image = ""
next_image = ""
available_images = []

for line in output.split("\n"):
if "current:" in line.lower():
parts = line.split(":")
if len(parts) > 1:
current_image = parts[1].strip()
elif "next:" in line.lower():
parts = line.split(":")
if len(parts) > 1:
next_image = parts[1].strip()
elif "available:" in line.lower():
continue
else:
available_images.append(line.strip())

logger.info("Current image: {}".format(current_image))
logger.info("Next image: {}".format(next_image))
logger.info("Available images: {}".format(available_images))
return {
"current": current_image or "",
"next": next_image or "",
"available": available_images or [],
}
25 changes: 11 additions & 14 deletions host_modules/reboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
MOD_NAME = 'reboot'
# Reboot method in reboot request
# Both enum and string representations are supported
REBOOTMETHOD_COLD_BOOT_VALUES = {1, "COLD"}
REBOOTMETHOD_WARM_BOOT_VALUES = {4, "WARM"}
REBOOT_METHOD_COLD_BOOT_VALUES = {1, "COLD"}
REBOOT_METHOD_WARM_BOOT_VALUES = {4, "WARM"}

# Timeout for SONiC Host Service to be killed during reboot
REBOOT_TIMEOUT = 260

EXECUTE_COLD_REBOOT_COMMAND = "sudo reboot"
EXECUTE_WARM_REBOOT_COMMAND = "/usr/local/bin/warm-reboot -v"
EXECUTE_WARM_REBOOT_COMMAND = "sudo warm-reboot"

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -51,30 +51,27 @@ def validate_reboot_request(self, reboot_request):
return 1, "Reboot request must contain a reboot method"

# Check whether reboot method is valid.
rebootmethod = reboot_request["method"]
valid_method = False
for values in [REBOOTMETHOD_COLD_BOOT_VALUES, REBOOTMETHOD_WARM_BOOT_VALUES]:
if rebootmethod in values:
valid_method = True
if not valid_method:
return 1, "Invalid reboot method: " + str(rebootmethod)
reboot_method = reboot_request["method"]
valid_reboot_method = REBOOT_METHOD_COLD_BOOT_VALUES | REBOOT_METHOD_WARM_BOOT_VALUES
if reboot_method not in valid_reboot_method:
return 1, "Invalid reboot method: " + str(reboot_method)

# Check whether delay is non-zero. delay key will not exist in reboot_request if it is zero
if "delay" in reboot_request and reboot_request["delay"] != 0:
return 1, "Delayed reboot is not supported"
return 0, ""

def execute_reboot(self, rebootmethod):
def execute_reboot(self, reboot_method):
"""Execute reboot and reset reboot_status_flag when reboot fails"""

if rebootmethod in REBOOTMETHOD_COLD_BOOT_VALUES:
if reboot_method in REBOOT_METHOD_COLD_BOOT_VALUES:
command = EXECUTE_COLD_REBOOT_COMMAND
logger.warning("%s: Issuing cold reboot", MOD_NAME)
elif rebootmethod in REBOOTMETHOD_WARM_BOOT_VALUES:
elif reboot_method in REBOOT_METHOD_WARM_BOOT_VALUES:
command = EXECUTE_WARM_REBOOT_COMMAND
logger.warning("%s: Issuing WARM reboot", MOD_NAME)
else:
logger.error("%s: Invalid reboot method: %d", MOD_NAME, rebootmethod)
logger.error("%s: Invalid reboot method: %d", MOD_NAME, reboot_method)
return

rc, stdout, stderr = _run_command(command)
Expand Down
22 changes: 0 additions & 22 deletions host_modules/systemd_service.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
"""Systemd service handler"""

from enum import Enum
from host_modules import host_service
import subprocess

MOD_NAME = 'systemd'
ALLOWED_SERVICES = ['snmp', 'swss', 'dhcp_relay', 'radv', 'restapi', 'lldp', 'sshd', 'pmon', 'rsyslog', 'telemetry']
EXIT_FAILURE = 1

# Define an Enum for Reboot Methods which are defined as in
# https://github.com/openconfig/gnoi/blob/main/system/system.pb.go#L27
class RebootMethod(Enum):
COLD = 1
HALT = 3

class SystemdService(host_service.HostModule):
"""
Expand Down Expand Up @@ -54,19 +48,3 @@ def stop_service(self, service):
if result.returncode:
msg = result.stderr.decode()
return result.returncode, msg

@host_service.method(host_service.bus_name(MOD_NAME), in_signature='i', out_signature='is')
def execute_reboot(self, rebootmethod):
if rebootmethod == RebootMethod.COLD:
cmd = ['/usr/local/bin/reboot']
elif rebootmethod == RebootMethod.HALT:
cmd = ['/usr/local/bin/reboot','-p']
else:
return EXIT_FAILURE, "{}: Invalid reboot method: {}".format(MOD_NAME, rebootmethod)

result = subprocess.run(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
msg = ''
if result.returncode:
msg = result.stderr.decode()

return result.returncode, msg
4 changes: 2 additions & 2 deletions scripts/caclmgrd
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ try:
import threading
import time
from sonic_py_common.general import getstatusoutput_noshell_pipe
from sonic_py_common import daemon_base, device_info, multi_asic
from sonic_py_common import logger, device_info, multi_asic
from swsscommon import swsscommon
except ImportError as err:
raise ImportError("%s - required module not found" % str(err))
Expand Down Expand Up @@ -64,7 +64,7 @@ def get_ipv4_networks_from_interface_table(table, intf_name):
# ============================== Classes ==============================


class ControlPlaneAclManager(daemon_base.DaemonBase):
class ControlPlaneAclManager(logger.Logger):
"""
Class which reads control plane ACL tables and rules from Config DB,
translates them into equivalent iptables commands and runs those
Expand Down
30 changes: 26 additions & 4 deletions scripts/hostcfgd
Original file line number Diff line number Diff line change
Expand Up @@ -1136,9 +1136,14 @@ class SshServer(object):
class KdumpCfg(object):
def __init__(self, CfgDb):
self.config_db = CfgDb
self.kdump_defaults = { "enabled" : "false",
"memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M",
"num_dumps": "3" }
self.kdump_defaults = {
"enabled": "false",
"memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M",
"num_dumps": "3",
"remote": "false", # New feature: remote, default is "false"
"SSH_KEY": "<user@server>", # New feature: SSH key, default value
"SSH_PATH": "<path>" # New feature: SSH path, default value
}

def load(self, kdump_table):
"""
Expand All @@ -1149,7 +1154,11 @@ class KdumpCfg(object):
for row in self.kdump_defaults:
value = self.kdump_defaults.get(row)
if not kdump_conf.get(row):
self.config_db.mod_entry("KDUMP", "config", {row : value})
self.config_db.mod_entry("KDUMP", "config", {row: value})

# Configure num_dumps
num_dumps = kdump_conf.get("num_dumps") or self.kdump_defaults["num_dumps"]
run_cmd(["sonic-kdump-config", "--num_dumps", num_dumps])

def kdump_update(self, key, data):
syslog.syslog(syslog.LOG_INFO, "Kdump global configuration update")
Expand Down Expand Up @@ -1179,6 +1188,19 @@ class KdumpCfg(object):
num_dumps = data.get("num_dumps")
run_cmd(["sonic-kdump-config", "--num_dumps", num_dumps])

# Remote option
remote = self.kdump_defaults["remote"]
if data.get("remote") is not None:
remote = data.get("remote")
run_cmd(["sonic-kdump-config", "--remote", remote])

# SSH key
if data.get("SSH_KEY") is not None and (data.get("SSH_KEY") != self.kdump_defaults["SSH_KEY"]):
run_cmd(["sonic-kdump-config", "--ssh_key", data.get("SSH_KEY")])
# SSH_PATH
if data.get("SSH_PATH") is not None and (data.get("SSH_PATH") != self.kdump_defaults["SSH_PATH"]):
run_cmd(["sonic-kdump-config", "--ssh_path", data.get("SSH_PATH")])

class NtpCfg(object):
"""
NtpCfg Config Daemon
Expand Down
4 changes: 2 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, reboot
from host_modules import config_engine, gcu, host_service, showtech, systemd_service, file_service, image_service, docker_service, reboot


def register_dbus():
Expand All @@ -21,7 +21,7 @@ def register_dbus():
'config': config_engine.Config('config'),
'gcu': gcu.GCU('gcu'),
'host_service': host_service.HostService('host_service'),
'reboot': reboot.Reboot('reboot'),
'reboot': reboot.Reboot('reboot'),
'showtech': showtech.Showtech('showtech'),
'systemd': systemd_service.SystemdService('systemd'),
'file_stat': file_service.FileService('file')
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
maintainer_email = 'jolevequ@microsoft.com',
packages = [
'host_modules',
'utils',
'utils',
],
scripts = [
'scripts/caclmgrd',
Expand Down
45 changes: 43 additions & 2 deletions tests/host_modules/docker_service_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import errno
import docker
import json
from unittest import mock
from host_modules.docker_service import DockerService

Expand Down Expand Up @@ -303,7 +304,7 @@ def test_docker_load_success(self, MockInit, MockBusName, MockSystemBus):
assert rc == 0, "Return code is wrong"
mock_docker_client.images.load.assert_called_once_with(MockOpen.return_value)
MockOpen.assert_called_once_with("image.tar", "rb")

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
Expand All @@ -317,4 +318,44 @@ def test_docker_load_file_not_found(self, MockInit, MockBusName, MockSystemBus):
rc, _ = docker_service.load("non_existent_image.tar")

assert rc == errno.ENOENT, "Return code is wrong"
MockOpen.assert_called_once_with("non_existent_image.tar", "rb")
MockOpen.assert_called_once_with("non_existent_image.tar", "rb")

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_list_success(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_container_1 = mock.Mock(id="1", status="running", image=mock.Mock(tags=["image1"], id="hash1"), labels={})
mock_container_2 = mock.Mock(id="2", status="exited", image=mock.Mock(tags=["image2"], id="hash2"), labels={})
# The name attribute needs to be explicitly set for the mock object.
mock_container_1.name = "container1"
mock_container_2.name = "container2"
mock_docker_client.containers.list.return_value = [
mock_container_1, mock_container_2
]

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, containers = docker_service.list(True, {})

assert rc == 0, "Return code is wrong {}".format(containers)
expected_containers = [
{
"id": "1",
"name": "container1",
"status": "running",
"image": "image1",
"labels": {},
"hash": "hash1",
},
{
"id": "2",
"name": "container2",
"status": "exited",
"image": "image2",
"labels": {},
"hash": "hash2",
},
]
assert json.loads(containers) == expected_containers, "Containers list is wrong"
mock_docker_client.containers.list.assert_called_once_with(all=True, filters={})
Loading

0 comments on commit 61e547d

Please sign in to comment.