Skip to content

Commit 89aead2

Browse files
authored
DBUS API for Containerz.StopContainer (sonic-net#179)
DBUS API for Containerz.StopContainer
1 parent c15aebc commit 89aead2

File tree

2 files changed

+352
-0
lines changed

2 files changed

+352
-0
lines changed

host_modules/docker_service.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Docker service handler"""
2+
3+
from host_modules import host_service
4+
import docker
5+
import signal
6+
import errno
7+
8+
MOD_NAME = "docker_service"
9+
10+
# The set of allowed containers that can be managed by this service.
11+
# First element is the image name, second element is the container name.
12+
ALLOWED_CONTAINERS = [
13+
("docker-syncd-brcm", "syncd"),
14+
("docker-acms", "acms"),
15+
("docker-sonic-gnmi", "gnmi"),
16+
("docker-sonic-telemetry", "telemetry"),
17+
("docker-snmp", "snmp"),
18+
("docker-platform-monitor", "pmon"),
19+
("docker-lldp", "lldp"),
20+
("docker-dhcp-relay", "dhcp_relay"),
21+
("docker-router-advertiser", "radv"),
22+
("docker-teamd", "teamd"),
23+
("docker-fpm-frr", "bgp"),
24+
("docker-orchagent", "swss"),
25+
("docker-sonic-restapi", "restapi"),
26+
("docker-eventd", "eventd"),
27+
("docker-database", "database"),
28+
]
29+
30+
31+
def is_allowed_container(container):
32+
"""
33+
Check if the container is allowed to be managed by this service.
34+
35+
Args:
36+
container (str): The container name.
37+
38+
Returns:
39+
bool: True if the container is allowed, False otherwise.
40+
"""
41+
for _, allowed_container in ALLOWED_CONTAINERS:
42+
if container == allowed_container:
43+
return True
44+
return False
45+
46+
47+
class DockerService(host_service.HostModule):
48+
"""
49+
DBus endpoint that executes the docker command
50+
"""
51+
52+
@host_service.method(
53+
host_service.bus_name(MOD_NAME), in_signature="s", out_signature="is"
54+
)
55+
def stop(self, container):
56+
"""
57+
Stop a running Docker container.
58+
59+
Args:
60+
container (str): The name or ID of the Docker container.
61+
62+
Returns:
63+
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
64+
"""
65+
try:
66+
client = docker.from_env()
67+
if not is_allowed_container(container):
68+
return (
69+
errno.EPERM,
70+
"Container {} is not allowed to be managed by this service.".format(
71+
container
72+
),
73+
)
74+
container = client.containers.get(container)
75+
container.stop()
76+
return 0, "Container {} has been stopped.".format(container.name)
77+
except docker.errors.NotFound:
78+
return errno.ENOENT, "Container {} does not exist.".format(container)
79+
except Exception as e:
80+
return 1, "Failed to stop container {}: {}".format(container, str(e))
81+
82+
@host_service.method(
83+
host_service.bus_name(MOD_NAME), in_signature="si", out_signature="is"
84+
)
85+
def kill(self, container, signal=signal.SIGKILL):
86+
"""
87+
Kill or send a signal to a running Docker container.
88+
89+
Args:
90+
container (str): The name or ID of the Docker container.
91+
signal (int): The signal to send. Defaults to SIGKILL.
92+
93+
Returns:
94+
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
95+
"""
96+
try:
97+
client = docker.from_env()
98+
if not is_allowed_container(container):
99+
return (
100+
errno.EPERM,
101+
"Container {} is not allowed to be managed by this service.".format(
102+
container
103+
),
104+
)
105+
container = client.containers.get(container)
106+
container.kill(signal=signal)
107+
return 0, "Container {} has been killed with signal {}.".format(
108+
container.name, signal
109+
)
110+
except docker.errors.NotFound:
111+
return errno.ENOENT, "Container {} does not exist.".format(container)
112+
except Exception as e:
113+
return 1, "Failed to kill container {}: {}".format(container, str(e))
114+
115+
@host_service.method(
116+
host_service.bus_name(MOD_NAME), in_signature="s", out_signature="is"
117+
)
118+
def restart(self, container):
119+
"""
120+
Restart a running Docker container.
121+
122+
Args:
123+
container (str): The name or ID of the Docker container.
124+
125+
Returns:
126+
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
127+
"""
128+
try:
129+
client = docker.from_env()
130+
if not is_allowed_container(container):
131+
return (
132+
errno.EPERM,
133+
"Container {} is not allowed to be managed by this service.".format(
134+
container
135+
),
136+
)
137+
container = client.containers.get(container)
138+
container.restart()
139+
return 0, "Container {} has been restarted.".format(container.name)
140+
except docker.errors.NotFound:
141+
return errno.ENOENT, "Container {} does not exist.".format(container)
142+
except Exception as e:
143+
return 1, "Failed to restart container {}: {}".format(container, str(e))
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import errno
2+
import docker
3+
from unittest import mock
4+
from host_modules.docker_service import DockerService
5+
6+
MOD_NAME = "docker_service"
7+
8+
9+
class TestDockerService(object):
10+
@mock.patch("dbus.SystemBus")
11+
@mock.patch("dbus.service.BusName")
12+
@mock.patch("dbus.service.Object.__init__")
13+
def test_docker_stop_success(self, MockInit, MockBusName, MockSystemBus):
14+
mock_docker_client = mock.Mock()
15+
mock_docker_client.containers.get.return_value.stop.return_value = None
16+
17+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
18+
docker_service = DockerService(MOD_NAME)
19+
rc, _ = docker_service.stop("syncd")
20+
21+
assert rc == 0, "Return code is wrong"
22+
mock_docker_client.containers.get.assert_called_once_with("syncd")
23+
mock_docker_client.containers.get.return_value.stop.assert_called_once()
24+
25+
@mock.patch("dbus.SystemBus")
26+
@mock.patch("dbus.service.BusName")
27+
@mock.patch("dbus.service.Object.__init__")
28+
def test_docker_stop_fail_disallowed(self, MockInit, MockBusName, MockSystemBus):
29+
mock_docker_client = mock.Mock()
30+
31+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
32+
docker_service = DockerService(MOD_NAME)
33+
rc, msg = docker_service.stop("bad-container")
34+
35+
assert rc == errno.EPERM, "Return code is wrong"
36+
assert (
37+
"not" in msg and "allowed" in msg
38+
), "Message should contain 'not' and 'allowed'"
39+
40+
@mock.patch("dbus.SystemBus")
41+
@mock.patch("dbus.service.BusName")
42+
@mock.patch("dbus.service.Object.__init__")
43+
def test_docker_stop_fail_not_exist(self, MockInit, MockBusName, MockSystemBus):
44+
mock_docker_client = mock.Mock()
45+
mock_docker_client.containers.get.side_effect = docker.errors.NotFound(
46+
"Container not found"
47+
)
48+
49+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
50+
docker_service = DockerService(MOD_NAME)
51+
rc, msg = docker_service.stop("syncd")
52+
53+
assert rc == errno.ENOENT, "Return code is wrong"
54+
assert (
55+
"not" in msg and "exist" in msg
56+
), "Message should contain 'not' and 'exist'"
57+
mock_docker_client.containers.get.assert_called_once_with("syncd")
58+
59+
@mock.patch("dbus.SystemBus")
60+
@mock.patch("dbus.service.BusName")
61+
@mock.patch("dbus.service.Object.__init__")
62+
def test_docker_stop_fail_api_error(self, MockInit, MockBusName, MockSystemBus):
63+
mock_docker_client = mock.Mock()
64+
mock_docker_client.containers.get.return_value.stop.side_effect = (
65+
docker.errors.APIError("API error")
66+
)
67+
68+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
69+
docker_service = DockerService(MOD_NAME)
70+
rc, msg = docker_service.stop("syncd")
71+
72+
assert rc != 0, "Return code is wrong"
73+
assert "API error" in msg, "Message should contain 'API error'"
74+
mock_docker_client.containers.get.assert_called_once_with("syncd")
75+
mock_docker_client.containers.get.return_value.stop.assert_called_once()
76+
77+
@mock.patch("dbus.SystemBus")
78+
@mock.patch("dbus.service.BusName")
79+
@mock.patch("dbus.service.Object.__init__")
80+
def test_docker_kill_success(self, MockInit, MockBusName, MockSystemBus):
81+
mock_docker_client = mock.Mock()
82+
mock_docker_client.containers.get.return_value.kill.return_value = None
83+
84+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
85+
docker_service = DockerService(MOD_NAME)
86+
rc, _ = docker_service.kill("syncd")
87+
88+
assert rc == 0, "Return code is wrong"
89+
mock_docker_client.containers.get.assert_called_once_with("syncd")
90+
mock_docker_client.containers.get.return_value.kill.assert_called_once()
91+
92+
@mock.patch("dbus.SystemBus")
93+
@mock.patch("dbus.service.BusName")
94+
@mock.patch("dbus.service.Object.__init__")
95+
def test_docker_kill_fail_disallowed(self, MockInit, MockBusName, MockSystemBus):
96+
mock_docker_client = mock.Mock()
97+
98+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
99+
docker_service = DockerService(MOD_NAME)
100+
rc, msg = docker_service.kill("bad-container")
101+
102+
assert rc == errno.EPERM, "Return code is wrong"
103+
assert (
104+
"not" in msg and "allowed" in msg
105+
), "Message should contain 'not' and 'allowed'"
106+
107+
@mock.patch("dbus.SystemBus")
108+
@mock.patch("dbus.service.BusName")
109+
@mock.patch("dbus.service.Object.__init__")
110+
def test_docker_kill_fail_not_found(self, MockInit, MockBusName, MockSystemBus):
111+
mock_docker_client = mock.Mock()
112+
mock_docker_client.containers.get.side_effect = docker.errors.NotFound(
113+
"Container not found"
114+
)
115+
116+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
117+
docker_service = DockerService(MOD_NAME)
118+
rc, msg = docker_service.kill("syncd")
119+
120+
assert rc == errno.ENOENT, "Return code is wrong"
121+
assert (
122+
"not" in msg and "exist" in msg
123+
), "Message should contain 'not' and 'exist'"
124+
mock_docker_client.containers.get.assert_called_once_with("syncd")
125+
126+
@mock.patch("dbus.SystemBus")
127+
@mock.patch("dbus.service.BusName")
128+
@mock.patch("dbus.service.Object.__init__")
129+
def test_docker_kill_fail_api_error(self, MockInit, MockBusName, MockSystemBus):
130+
mock_docker_client = mock.Mock()
131+
mock_docker_client.containers.get.return_value.kill.side_effect = (
132+
docker.errors.APIError("API error")
133+
)
134+
135+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
136+
docker_service = DockerService(MOD_NAME)
137+
rc, msg = docker_service.kill("syncd")
138+
139+
assert rc != 0, "Return code is wrong"
140+
assert "API error" in msg, "Message should contain 'API error'"
141+
mock_docker_client.containers.get.assert_called_once_with("syncd")
142+
mock_docker_client.containers.get.return_value.kill.assert_called_once()
143+
144+
@mock.patch("dbus.SystemBus")
145+
@mock.patch("dbus.service.BusName")
146+
@mock.patch("dbus.service.Object.__init__")
147+
def test_docker_restart_success(self, MockInit, MockBusName, MockSystemBus):
148+
mock_docker_client = mock.Mock()
149+
mock_docker_client.containers.get.return_value.restart.return_value = None
150+
151+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
152+
docker_service = DockerService(MOD_NAME)
153+
rc, _ = docker_service.restart("syncd")
154+
155+
assert rc == 0, "Return code is wrong"
156+
mock_docker_client.containers.get.assert_called_once_with("syncd")
157+
mock_docker_client.containers.get.return_value.restart.assert_called_once()
158+
159+
@mock.patch("dbus.SystemBus")
160+
@mock.patch("dbus.service.BusName")
161+
@mock.patch("dbus.service.Object.__init__")
162+
def test_docker_restart_fail_disallowed(self, MockInit, MockBusName, MockSystemBus):
163+
mock_docker_client = mock.Mock()
164+
165+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
166+
docker_service = DockerService(MOD_NAME)
167+
rc, msg = docker_service.restart("bad-container")
168+
169+
assert rc == errno.EPERM, "Return code is wrong"
170+
assert (
171+
"not" in msg and "allowed" in msg
172+
), "Message should contain 'not' and 'allowed'"
173+
174+
@mock.patch("dbus.SystemBus")
175+
@mock.patch("dbus.service.BusName")
176+
@mock.patch("dbus.service.Object.__init__")
177+
def test_docker_restart_fail_not_found(self, MockInit, MockBusName, MockSystemBus):
178+
mock_docker_client = mock.Mock()
179+
mock_docker_client.containers.get.side_effect = docker.errors.NotFound(
180+
"Container not found"
181+
)
182+
183+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
184+
docker_service = DockerService(MOD_NAME)
185+
rc, msg = docker_service.restart("syncd")
186+
187+
assert rc == errno.ENOENT, "Return code is wrong"
188+
assert (
189+
"not" in msg and "exist" in msg
190+
), "Message should contain 'not' and 'exist'"
191+
mock_docker_client.containers.get.assert_called_once_with("syncd")
192+
193+
@mock.patch("dbus.SystemBus")
194+
@mock.patch("dbus.service.BusName")
195+
@mock.patch("dbus.service.Object.__init__")
196+
def test_docker_restart_fail_api_error(self, MockInit, MockBusName, MockSystemBus):
197+
mock_docker_client = mock.Mock()
198+
mock_docker_client.containers.get.return_value.restart.side_effect = (
199+
docker.errors.APIError("API error")
200+
)
201+
202+
with mock.patch.object(docker, "from_env", return_value=mock_docker_client):
203+
docker_service = DockerService(MOD_NAME)
204+
rc, msg = docker_service.restart("syncd")
205+
206+
assert rc != 0, "Return code is wrong"
207+
assert "API error" in msg, "Message should contain 'API error'"
208+
mock_docker_client.containers.get.assert_called_once_with("syncd")
209+
mock_docker_client.containers.get.return_value.restart.assert_called_once()

0 commit comments

Comments
 (0)