Skip to content

Commit facaca3

Browse files
author
Rajat Gupta
committed
Add integration tests for systemd
Signed-off-by: Rajat Gupta <[email protected]>
1 parent 890612e commit facaca3

File tree

3 files changed

+325
-0
lines changed

3 files changed

+325
-0
lines changed

qa/systemd-test/build.gradle

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import org.opensearch.gradle.Architecture
2+
import org.opensearch.gradle.VersionProperties
3+
import org.opensearch.gradle.testfixtures.TestFixturesPlugin
4+
5+
apply plugin: 'opensearch.standalone-rest-test'
6+
apply plugin: 'opensearch.test.fixtures'
7+
8+
testFixtures.useFixture()
9+
10+
dockerCompose {
11+
useComposeFiles = ['docker-compose.yml']
12+
}
13+
14+
15+
tasks.register("integTest", Test) {
16+
outputs.doNotCacheIf('Build cache is disabled for Docker tests') { true }
17+
maxParallelForks = '1'
18+
include '**/*IT.class'
19+
}
20+
21+
tasks.named("check").configure { dependsOn "integTest" }
22+
23+
tasks.named("integTest").configure {
24+
dependsOn "composeUp"
25+
finalizedBy "composeDown"
26+
}

qa/systemd-test/docker-compose.yml

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
services:
2+
# self-contained systemd example: run 'docker-compose up' to see it
3+
amazonlinux:
4+
image: opensearch-systemd-test
5+
container_name: opensearch-systemd-test-container
6+
build:
7+
dockerfile_inline: |
8+
FROM amazonlinux:2023
9+
# install systemd
10+
RUN dnf -y install systemd && dnf clean all
11+
# in practice, you'd COPY in the RPM you want to test right here
12+
RUN dnf -y install https://artifacts.opensearch.org/releases/bundle/opensearch/2.18.0/opensearch-2.18.0-linux-x64.rpm && dnf clean all
13+
# add a test-user
14+
RUN useradd -ms /bin/bash testuser
15+
# no colors
16+
ENV SYSTEMD_COLORS=0
17+
# no escapes
18+
ENV SYSTEMD_URLIFY=0
19+
# explicitly specify docker virtualization
20+
ENV container=docker
21+
# for debugging systemd issues in container, you want this, but it is very loud!
22+
# ENV SYSTEMD_LOG_LEVEL=debug
23+
# plumb journald logs to stdout
24+
COPY <<EOF /etc/systemd/journald.conf
25+
[Journal]
26+
ForwardToConsole=yes
27+
EOF
28+
# start systemd as PID 1
29+
CMD ["/sbin/init"]
30+
# enable opensearch service
31+
RUN systemctl enable opensearch
32+
# shutdown systemd properly
33+
STOPSIGNAL SIGRTMIN+3
34+
# disable security plugin, as i don't configure SSL (but could be done with openssl or whatever right here)
35+
RUN echo "plugins.security.disabled: true" >> /etc/opensearch/opensearch.yml
36+
RUN echo "network.host: 0.0.0.0" >> /etc/opensearch/opensearch.yml
37+
RUN echo "discovery.type: single-node" >> /etc/opensearch/opensearch.yml
38+
# provide /dev/console for journal logs to go to stdout
39+
tty: true
40+
# capabilities to allow systemd to sandbox
41+
cap_add:
42+
# https://systemd.io/CONTAINER_INTERFACE/#what-you-shouldnt-do bullet 1
43+
- SYS_ADMIN
44+
# https://systemd.io/CONTAINER_INTERFACE/#what-you-shouldnt-do bullet 2
45+
- MKNOD
46+
# evil, but best you can do on docker? podman is better here.
47+
cgroup: host
48+
volumes:
49+
- /sys/fs/cgroup:/sys/fs/cgroup
50+
- ../../distribution/packages/src/common/systemd/opensearch.service:/etc/systemd/system/opensearch.service
51+
# tmpfs mounts for systemd
52+
tmpfs:
53+
- /run
54+
- /run/lock
55+
# health check for opensearch
56+
ports:
57+
- "9200:9200"
58+
- "9300:9300"
59+
privileged: true
60+
healthcheck:
61+
test: ["CMD", "curl", "-f", "http://localhost:9200/_cluster/health"]
62+
start_period: 15s
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
/*
10+
* Licensed to Elasticsearch under one or more contributor
11+
* license agreements. See the NOTICE file distributed with
12+
* this work for additional information regarding copyright
13+
* ownership. Elasticsearch licenses this file to you under
14+
* the Apache License, Version 2.0 (the "License"); you may
15+
* not use this file except in compliance with the License.
16+
* You may obtain a copy of the License at
17+
*
18+
* http://www.apache.org/licenses/LICENSE-2.0
19+
*
20+
* Unless required by applicable law or agreed to in writing,
21+
* software distributed under the License is distributed on an
22+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23+
* KIND, either express or implied. See the License for the
24+
* specific language governing permissions and limitations
25+
* under the License.
26+
*/
27+
/*
28+
* Modifications Copyright OpenSearch Contributors. See
29+
* GitHub history for details.
30+
*/
31+
32+
package org.opensearch.systemdinteg;
33+
34+
import org.apache.hc.core5.http.HttpHeaders;
35+
import org.apache.hc.core5.http.HttpHost;
36+
import org.apache.hc.core5.http.HttpStatus;
37+
import org.junit.After;
38+
import org.junit.Before;
39+
import org.junit.BeforeClass;
40+
import org.junit.Test;
41+
import java.io.IOException;
42+
import java.io.InputStream;
43+
import java.io.InputStreamReader;
44+
import java.io.BufferedReader;
45+
import java.net.HttpURLConnection;
46+
import java.net.URL;
47+
import static org.junit.Assert.*;
48+
import static org.junit.Assert.assertTrue;
49+
import static org.junit.Assert.assertFalse;
50+
51+
52+
public class SystemdIT {
53+
private static final String OPENSEARCH_URL = "http://localhost:9200"; // OpenSearch URL (port 9200)
54+
private static String containerId;
55+
private static String opensearchPid;
56+
private static final String CONTAINER_NAME = "opensearch-systemd-test-container";
57+
58+
@BeforeClass
59+
public static void setup() throws IOException, InterruptedException {
60+
containerId = getContainerId();
61+
62+
String status = executeCommand("docker exec " + containerId + " systemctl status opensearch", "Failed to check OpenSearch status");
63+
64+
opensearchPid = getOpenSearchPid();
65+
66+
if (opensearchPid.isEmpty()) {
67+
throw new RuntimeException("Failed to find OpenSearch process ID");
68+
}
69+
}
70+
71+
private static String getContainerId() throws IOException, InterruptedException {
72+
return executeCommand("docker ps -qf name=" + CONTAINER_NAME, "OpenSearch container '" + CONTAINER_NAME + "' is not running");
73+
}
74+
75+
private static String getOpenSearchPid() throws IOException, InterruptedException {
76+
String command = "docker exec " + containerId + " systemctl show --property=MainPID opensearch";
77+
String output = executeCommand(command, "Failed to get OpenSearch PID");
78+
return output.replace("MainPID=", "").trim();
79+
}
80+
81+
private boolean checkPathExists(String path) throws IOException, InterruptedException {
82+
String command = String.format("docker exec %s test -e %s && echo true || echo false", containerId, path);
83+
return Boolean.parseBoolean(executeCommand(command, "Failed to check path existence"));
84+
}
85+
86+
private boolean checkPathReadable(String path) throws IOException, InterruptedException {
87+
String command = String.format("docker exec %s su opensearch -s /bin/sh -c 'test -r %s && echo true || echo false'", containerId, path);
88+
return Boolean.parseBoolean(executeCommand(command, "Failed to check read permission"));
89+
}
90+
91+
private boolean checkPathWritable(String path) throws IOException, InterruptedException {
92+
String command = String.format("docker exec %s su opensearch -s /bin/sh -c 'test -w %s && echo true || echo false'", containerId, path);
93+
return Boolean.parseBoolean(executeCommand(command, "Failed to check write permission"));
94+
}
95+
96+
private String getPathOwnership(String path) throws IOException, InterruptedException {
97+
String command = String.format("docker exec %s stat -c '%%U:%%G' %s", containerId, path);
98+
return executeCommand(command, "Failed to get path ownership");
99+
}
100+
101+
private static String executeCommand(String command, String errorMessage) throws IOException, InterruptedException {
102+
Process process = Runtime.getRuntime().exec(new String[]{"bash", "-c", command});
103+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
104+
StringBuilder output = new StringBuilder();
105+
String line;
106+
while ((line = reader.readLine()) != null) {
107+
output.append(line).append("\n");
108+
}
109+
if (process.waitFor() != 0) {
110+
throw new RuntimeException(errorMessage);
111+
}
112+
return output.toString().trim();
113+
}
114+
}
115+
116+
@Test
117+
public void testClusterHealth() throws IOException {
118+
HttpURLConnection healthCheck = (HttpURLConnection) new URL(OPENSEARCH_URL + "/_cluster/health").openConnection();
119+
healthCheck.setRequestMethod("GET");
120+
int healthResponseCode = healthCheck.getResponseCode();
121+
assertTrue(healthResponseCode == HttpURLConnection.HTTP_OK);
122+
}
123+
124+
@Test
125+
public void testMaxProcesses() throws IOException, InterruptedException {
126+
String limits = executeCommand("docker exec " + containerId + " cat /proc/" + opensearchPid + "/limits", "Failed to read process limits");
127+
assertTrue("Max processes limit should be 4096 or unlimited",
128+
limits.contains("Max processes 4096 4096") ||
129+
limits.contains("Max processes unlimited unlimited"));
130+
}
131+
132+
@Test
133+
public void testFileDescriptorLimit() throws IOException, InterruptedException {
134+
String limits = executeCommand("docker exec " + containerId + " cat /proc/" + opensearchPid + "/limits", "Failed to read process limits");
135+
assertTrue("File descriptor limit should be at least 65535",
136+
limits.contains("Max open files 65535 65535") ||
137+
limits.contains("Max open files unlimited unlimited"));
138+
}
139+
140+
141+
@Test
142+
public void testSystemCallFilter() throws IOException, InterruptedException {
143+
// Check if Seccomp is enabled
144+
String seccomp = executeCommand("docker exec " + containerId + " grep Seccomp /proc/" + opensearchPid + "/status", "Failed to read Seccomp status");
145+
assertFalse("Seccomp should be enabled", seccomp.contains("0"));
146+
147+
// Test specific system calls that should be blocked
148+
String rebootResult = executeCommand("docker exec " + containerId + " su opensearch -c 'kill -s SIGHUP 1' 2>&1 || echo 'Operation not permitted'", "Failed to test reboot system call");
149+
assertTrue("Reboot system call should be blocked", rebootResult.contains("Operation not permitted"));
150+
151+
String swapResult = executeCommand("docker exec " + containerId + " su opensearch -c 'swapon -a' 2>&1 || echo 'Operation not permitted'", "Failed to test swap system call");
152+
assertTrue("Swap system call should be blocked", swapResult.contains("Operation not permitted"));
153+
}
154+
155+
156+
@Test
157+
public void testReadOnlyPaths() throws IOException, InterruptedException {
158+
String[] readOnlyPaths = {
159+
"/etc/os-release", "/usr/lib/os-release", "/etc/system-release",
160+
"/proc/self/mountinfo", "/proc/diskstats",
161+
"/proc/self/cgroup", "/sys/fs/cgroup/cpu", "/sys/fs/cgroup/cpu/-",
162+
"/sys/fs/cgroup/cpuacct", "/sys/fs/cgroup/cpuacct/-",
163+
"/sys/fs/cgroup/memory", "/sys/fs/cgroup/memory/-"
164+
};
165+
166+
for (String path : readOnlyPaths) {
167+
if (checkPathExists(path)) {
168+
assertTrue("Path should be readable: " + path, checkPathReadable(path));
169+
assertFalse("Path should not be writable: " + path, checkPathWritable(path));
170+
}
171+
}
172+
}
173+
174+
@Test
175+
public void testReadWritePaths() throws IOException, InterruptedException {
176+
String[] readWritePaths = {"/var/log/opensearch", "/var/lib/opensearch"};
177+
for (String path : readWritePaths) {
178+
assertTrue("Path should exist: " + path, checkPathExists(path));
179+
assertTrue("Path should be readable: " + path, checkPathReadable(path));
180+
assertTrue("Path should be writable: " + path, checkPathWritable(path));
181+
assertEquals("Path should be owned by opensearch:opensearch", "opensearch:opensearch", getPathOwnership(path));
182+
}
183+
}
184+
185+
@Test
186+
public void testProcessExit() throws IOException, InterruptedException {
187+
188+
String scriptContent = "#!/bin/sh\n" +
189+
"if [ $# -ne 1 ]; then\n" +
190+
" echo \"Usage: $0 <PID>\"\n" +
191+
" exit 1\n" +
192+
"fi\n" +
193+
"if kill -15 $1 2>/dev/null; then\n" +
194+
" echo \"SIGTERM signal sent to process $1\"\n" +
195+
"else\n" +
196+
" echo \"Failed to send SIGTERM to process $1\"\n" +
197+
"fi\n" +
198+
"sleep 2\n" +
199+
"if kill -0 $1 2>/dev/null; then\n" +
200+
" echo \"Process $1 is still running\"\n" +
201+
"else\n" +
202+
" echo \"Process $1 has terminated\"\n" +
203+
"fi";
204+
205+
String[] command = {
206+
"docker",
207+
"exec",
208+
"-u", "testuser",
209+
containerId,
210+
"sh",
211+
"-c",
212+
"echo '" + scriptContent.replace("'", "'\"'\"'") + "' > /tmp/terminate.sh && chmod +x /tmp/terminate.sh && /tmp/terminate.sh " + opensearchPid
213+
};
214+
215+
ProcessBuilder processBuilder = new ProcessBuilder(command);
216+
Process process = processBuilder.start();
217+
218+
// Wait a moment for any potential termination to take effect
219+
Thread.sleep(2000);
220+
221+
// Check if the OpenSearch process is still running
222+
String processCheck = executeCommand(
223+
"docker exec " + containerId + " kill -0 " + opensearchPid + " 2>/dev/null && echo 'Running' || echo 'Not running'",
224+
"Failed to check process status"
225+
);
226+
227+
// Verify the OpenSearch service status
228+
String serviceStatus = executeCommand(
229+
"docker exec " + containerId + " systemctl is-active opensearch",
230+
"Failed to check OpenSearch service status"
231+
);
232+
233+
assertTrue("OpenSearch process should still be running", processCheck.contains("Running"));
234+
assertEquals("OpenSearch service should be active", "active", serviceStatus.trim());
235+
}
236+
237+
}

0 commit comments

Comments
 (0)