Skip to content

Commit

Permalink
DBUS API for Containerz.StopContainer (#179)
Browse files Browse the repository at this point in the history
DBUS API for Containerz.StopContainer
  • Loading branch information
hdwhdw authored Nov 22, 2024
1 parent c15aebc commit 89aead2
Show file tree
Hide file tree
Showing 2 changed files with 352 additions and 0 deletions.
143 changes: 143 additions & 0 deletions host_modules/docker_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""Docker service handler"""

from host_modules import host_service
import docker
import signal
import errno

MOD_NAME = "docker_service"

# The set of allowed containers that can be managed by this service.
# First element is the image name, second element is the container name.
ALLOWED_CONTAINERS = [
("docker-syncd-brcm", "syncd"),
("docker-acms", "acms"),
("docker-sonic-gnmi", "gnmi"),
("docker-sonic-telemetry", "telemetry"),
("docker-snmp", "snmp"),
("docker-platform-monitor", "pmon"),
("docker-lldp", "lldp"),
("docker-dhcp-relay", "dhcp_relay"),
("docker-router-advertiser", "radv"),
("docker-teamd", "teamd"),
("docker-fpm-frr", "bgp"),
("docker-orchagent", "swss"),
("docker-sonic-restapi", "restapi"),
("docker-eventd", "eventd"),
("docker-database", "database"),
]


def is_allowed_container(container):
"""
Check if the container is allowed to be managed by this service.
Args:
container (str): The container name.
Returns:
bool: True if the container is allowed, False otherwise.
"""
for _, allowed_container in ALLOWED_CONTAINERS:
if container == allowed_container:
return True
return False


class DockerService(host_service.HostModule):
"""
DBus endpoint that executes the docker command
"""

@host_service.method(
host_service.bus_name(MOD_NAME), in_signature="s", out_signature="is"
)
def stop(self, container):
"""
Stop a running Docker container.
Args:
container (str): The name or ID of the Docker container.
Returns:
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
"""
try:
client = docker.from_env()
if not is_allowed_container(container):
return (
errno.EPERM,
"Container {} is not allowed to be managed by this service.".format(
container
),
)
container = client.containers.get(container)
container.stop()
return 0, "Container {} has been stopped.".format(container.name)
except docker.errors.NotFound:
return errno.ENOENT, "Container {} does not exist.".format(container)
except Exception as e:
return 1, "Failed to stop container {}: {}".format(container, str(e))

@host_service.method(
host_service.bus_name(MOD_NAME), in_signature="si", out_signature="is"
)
def kill(self, container, signal=signal.SIGKILL):
"""
Kill or send a signal to a running Docker container.
Args:
container (str): The name or ID of the Docker container.
signal (int): The signal to send. Defaults to SIGKILL.
Returns:
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
"""
try:
client = docker.from_env()
if not is_allowed_container(container):
return (
errno.EPERM,
"Container {} is not allowed to be managed by this service.".format(
container
),
)
container = client.containers.get(container)
container.kill(signal=signal)
return 0, "Container {} has been killed with signal {}.".format(
container.name, signal
)
except docker.errors.NotFound:
return errno.ENOENT, "Container {} does not exist.".format(container)
except Exception as e:
return 1, "Failed to kill container {}: {}".format(container, str(e))

@host_service.method(
host_service.bus_name(MOD_NAME), in_signature="s", out_signature="is"
)
def restart(self, container):
"""
Restart a running Docker container.
Args:
container (str): The name or ID of the Docker container.
Returns:
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
"""
try:
client = docker.from_env()
if not is_allowed_container(container):
return (
errno.EPERM,
"Container {} is not allowed to be managed by this service.".format(
container
),
)
container = client.containers.get(container)
container.restart()
return 0, "Container {} has been restarted.".format(container.name)
except docker.errors.NotFound:
return errno.ENOENT, "Container {} does not exist.".format(container)
except Exception as e:
return 1, "Failed to restart container {}: {}".format(container, str(e))
209 changes: 209 additions & 0 deletions tests/host_modules/docker_service_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import errno
import docker
from unittest import mock
from host_modules.docker_service import DockerService

MOD_NAME = "docker_service"


class TestDockerService(object):
@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_stop_success(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_docker_client.containers.get.return_value.stop.return_value = None

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, _ = docker_service.stop("syncd")

assert rc == 0, "Return code is wrong"
mock_docker_client.containers.get.assert_called_once_with("syncd")
mock_docker_client.containers.get.return_value.stop.assert_called_once()

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_stop_fail_disallowed(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, msg = docker_service.stop("bad-container")

assert rc == errno.EPERM, "Return code is wrong"
assert (
"not" in msg and "allowed" in msg
), "Message should contain 'not' and 'allowed'"

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_stop_fail_not_exist(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_docker_client.containers.get.side_effect = docker.errors.NotFound(
"Container not found"
)

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, msg = docker_service.stop("syncd")

assert rc == errno.ENOENT, "Return code is wrong"
assert (
"not" in msg and "exist" in msg
), "Message should contain 'not' and 'exist'"
mock_docker_client.containers.get.assert_called_once_with("syncd")

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_stop_fail_api_error(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_docker_client.containers.get.return_value.stop.side_effect = (
docker.errors.APIError("API error")
)

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, msg = docker_service.stop("syncd")

assert rc != 0, "Return code is wrong"
assert "API error" in msg, "Message should contain 'API error'"
mock_docker_client.containers.get.assert_called_once_with("syncd")
mock_docker_client.containers.get.return_value.stop.assert_called_once()

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_kill_success(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_docker_client.containers.get.return_value.kill.return_value = None

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, _ = docker_service.kill("syncd")

assert rc == 0, "Return code is wrong"
mock_docker_client.containers.get.assert_called_once_with("syncd")
mock_docker_client.containers.get.return_value.kill.assert_called_once()

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_kill_fail_disallowed(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, msg = docker_service.kill("bad-container")

assert rc == errno.EPERM, "Return code is wrong"
assert (
"not" in msg and "allowed" in msg
), "Message should contain 'not' and 'allowed'"

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_kill_fail_not_found(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_docker_client.containers.get.side_effect = docker.errors.NotFound(
"Container not found"
)

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, msg = docker_service.kill("syncd")

assert rc == errno.ENOENT, "Return code is wrong"
assert (
"not" in msg and "exist" in msg
), "Message should contain 'not' and 'exist'"
mock_docker_client.containers.get.assert_called_once_with("syncd")

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_kill_fail_api_error(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_docker_client.containers.get.return_value.kill.side_effect = (
docker.errors.APIError("API error")
)

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, msg = docker_service.kill("syncd")

assert rc != 0, "Return code is wrong"
assert "API error" in msg, "Message should contain 'API error'"
mock_docker_client.containers.get.assert_called_once_with("syncd")
mock_docker_client.containers.get.return_value.kill.assert_called_once()

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_restart_success(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_docker_client.containers.get.return_value.restart.return_value = None

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, _ = docker_service.restart("syncd")

assert rc == 0, "Return code is wrong"
mock_docker_client.containers.get.assert_called_once_with("syncd")
mock_docker_client.containers.get.return_value.restart.assert_called_once()

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_restart_fail_disallowed(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, msg = docker_service.restart("bad-container")

assert rc == errno.EPERM, "Return code is wrong"
assert (
"not" in msg and "allowed" in msg
), "Message should contain 'not' and 'allowed'"

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_restart_fail_not_found(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_docker_client.containers.get.side_effect = docker.errors.NotFound(
"Container not found"
)

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, msg = docker_service.restart("syncd")

assert rc == errno.ENOENT, "Return code is wrong"
assert (
"not" in msg and "exist" in msg
), "Message should contain 'not' and 'exist'"
mock_docker_client.containers.get.assert_called_once_with("syncd")

@mock.patch("dbus.SystemBus")
@mock.patch("dbus.service.BusName")
@mock.patch("dbus.service.Object.__init__")
def test_docker_restart_fail_api_error(self, MockInit, MockBusName, MockSystemBus):
mock_docker_client = mock.Mock()
mock_docker_client.containers.get.return_value.restart.side_effect = (
docker.errors.APIError("API error")
)

with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
docker_service = DockerService(MOD_NAME)
rc, msg = docker_service.restart("syncd")

assert rc != 0, "Return code is wrong"
assert "API error" in msg, "Message should contain 'API error'"
mock_docker_client.containers.get.assert_called_once_with("syncd")
mock_docker_client.containers.get.return_value.restart.assert_called_once()

0 comments on commit 89aead2

Please sign in to comment.