/
ServerCli.java
267 lines (235 loc) · 11.2 KB
/
ServerCli.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.server.cli;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import joptsimple.OptionSpecBuilder;
import joptsimple.util.PathConverter;
import org.elasticsearch.Build;
import org.elasticsearch.bootstrap.ServerArgs;
import org.elasticsearch.cli.CliToolProvider;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.ProcessInfo;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.cli.EnvironmentAwareCommand;
import org.elasticsearch.common.settings.SecureSettings;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.env.Environment;
import org.elasticsearch.monitor.jvm.JvmInfo;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Locale;
/**
* The main CLI for running Elasticsearch.
*/
class ServerCli extends EnvironmentAwareCommand {
private final OptionSpecBuilder versionOption;
private final OptionSpecBuilder daemonizeOption;
private final OptionSpec<Path> pidfileOption;
private final OptionSpecBuilder quietOption;
private final OptionSpec<String> enrollmentTokenOption;
private volatile ServerProcess server;
// visible for testing
ServerCli() {
super("Starts Elasticsearch"); // we configure logging later, so we override the base class from configuring logging
versionOption = parser.acceptsAll(Arrays.asList("V", "version"), "Prints Elasticsearch version information and exits");
daemonizeOption = parser.acceptsAll(Arrays.asList("d", "daemonize"), "Starts Elasticsearch in the background")
.availableUnless(versionOption);
pidfileOption = parser.acceptsAll(Arrays.asList("p", "pidfile"), "Creates a pid file in the specified path on start")
.availableUnless(versionOption)
.withRequiredArg()
.withValuesConvertedBy(new PathConverter());
quietOption = parser.acceptsAll(Arrays.asList("q", "quiet"), "Turns off standard output/error streams logging in console")
.availableUnless(versionOption)
.availableUnless(daemonizeOption);
enrollmentTokenOption = parser.accepts("enrollment-token", "An existing enrollment token for securely joining a cluster")
.availableUnless(versionOption)
.withRequiredArg();
}
@Override
public void execute(Terminal terminal, OptionSet options, Environment env, ProcessInfo processInfo) throws Exception {
if (options.nonOptionArguments().isEmpty() == false) {
throw new UserException(ExitCodes.USAGE, "Positional arguments not allowed, found " + options.nonOptionArguments());
}
if (options.has(versionOption)) {
printVersion(terminal);
return;
}
validateConfig(options, env);
var secureSettingsLoader = secureSettingsLoader(env);
try (
var loadedSecrets = secureSettingsLoader.load(env, terminal);
var password = (loadedSecrets.password().isPresent()) ? loadedSecrets.password().get() : new SecureString(new char[0]);
) {
SecureSettings secrets = loadedSecrets.secrets();
if (secureSettingsLoader.supportsSecurityAutoConfiguration()) {
env = autoConfigureSecurity(terminal, options, processInfo, env, password);
// reload or create the secrets
secrets = secureSettingsLoader.bootstrap(env, password);
}
// we should have a loaded or bootstrapped secure settings at this point
if (secrets == null) {
throw new UserException(ExitCodes.CONFIG, "Elasticsearch secure settings not configured");
}
// install/remove plugins from elasticsearch-plugins.yml
syncPlugins(terminal, env, processInfo);
ServerArgs args = createArgs(options, env, secrets, processInfo);
this.server = startServer(terminal, processInfo, args);
}
if (options.has(daemonizeOption)) {
server.detach();
return;
}
// we are running in the foreground, so wait for the server to exit
int exitCode = server.waitFor();
onExit(exitCode);
}
/**
* A post-exit hook to perform additional processing before the command terminates
* @param exitCode the server process exit code
*/
protected void onExit(int exitCode) throws UserException {
if (exitCode != ExitCodes.OK) {
throw new UserException(exitCode, "Elasticsearch exited unexpectedly");
}
}
private static void printVersion(Terminal terminal) {
final String versionOutput = String.format(
Locale.ROOT,
"Version: %s, Build: %s/%s/%s, JVM: %s",
Build.current().qualifiedVersion(),
Build.current().type().displayName(),
Build.current().hash(),
Build.current().date(),
JvmInfo.jvmInfo().version()
);
terminal.println(versionOutput);
}
private void validateConfig(OptionSet options, Environment env) throws UserException {
if (options.valuesOf(enrollmentTokenOption).size() > 1) {
throw new UserException(ExitCodes.USAGE, "Multiple --enrollment-token parameters are not allowed");
}
Path log4jConfig = env.configFile().resolve("log4j2.properties");
if (Files.exists(log4jConfig) == false) {
throw new UserException(ExitCodes.CONFIG, "Missing logging config file at " + log4jConfig);
}
}
// Autoconfiguration of SecureSettings is currently only supported for KeyStore based secure settings
// package private for testing
Environment autoConfigureSecurity(
Terminal terminal,
OptionSet options,
ProcessInfo processInfo,
Environment env,
SecureString keystorePassword
) throws Exception {
assert secureSettingsLoader(env) instanceof KeyStoreLoader;
String autoConfigLibs = "modules/x-pack-core,modules/x-pack-security,lib/tools/security-cli";
Command cmd = loadTool("auto-configure-node", autoConfigLibs);
assert cmd instanceof EnvironmentAwareCommand;
@SuppressWarnings("raw")
var autoConfigNode = (EnvironmentAwareCommand) cmd;
final String[] autoConfigArgs;
if (options.has(enrollmentTokenOption)) {
autoConfigArgs = new String[] { "--enrollment-token", options.valueOf(enrollmentTokenOption) };
} else {
autoConfigArgs = new String[0];
}
OptionSet autoConfigOptions = autoConfigNode.parseOptions(autoConfigArgs);
boolean changed = true;
try (var autoConfigTerminal = new KeystorePasswordTerminal(terminal, keystorePassword.clone())) {
autoConfigNode.execute(autoConfigTerminal, autoConfigOptions, env, processInfo);
} catch (UserException e) {
boolean okCode = switch (e.exitCode) {
// these exit codes cover the cases where auto-conf cannot run but the node should NOT be prevented from starting as usual
// e.g. the node is restarted, is already configured in an incompatible way, or the file system permissions do not allow it
case ExitCodes.CANT_CREATE, ExitCodes.CONFIG, ExitCodes.NOOP -> true;
default -> false;
};
if (options.has(enrollmentTokenOption) == false && okCode) {
// we still want to print the error, just don't fail startup
if (e.getMessage() != null) {
terminal.errorPrintln(e.getMessage());
}
changed = false;
} else {
throw e;
}
}
if (changed) {
// reload settings since auto security changed them
env = createEnv(options, processInfo);
}
return env;
}
// package private for testing
void syncPlugins(Terminal terminal, Environment env, ProcessInfo processInfo) throws Exception {
String pluginCliLibs = "lib/tools/plugin-cli";
Command cmd = loadTool("sync-plugins", pluginCliLibs);
assert cmd instanceof EnvironmentAwareCommand;
@SuppressWarnings("raw")
var syncPlugins = (EnvironmentAwareCommand) cmd;
syncPlugins.execute(terminal, syncPlugins.parseOptions(new String[0]), env, processInfo);
}
private static void validatePidFile(Path pidFile) throws UserException {
Path parent = pidFile.getParent();
if (parent != null && Files.exists(parent) && Files.isDirectory(parent) == false) {
throw new UserException(ExitCodes.USAGE, "pid file parent [" + parent + "] exists but is not a directory");
}
if (Files.exists(pidFile) && Files.isRegularFile(pidFile) == false) {
throw new UserException(ExitCodes.USAGE, pidFile + " exists but is not a regular file");
}
}
private ServerArgs createArgs(OptionSet options, Environment env, SecureSettings secrets, ProcessInfo processInfo)
throws UserException {
boolean daemonize = options.has(daemonizeOption);
boolean quiet = options.has(quietOption);
Path pidFile = null;
if (options.has(pidfileOption)) {
pidFile = options.valueOf(pidfileOption);
if (pidFile.isAbsolute() == false) {
pidFile = processInfo.workingDir().resolve(pidFile.toString()).toAbsolutePath();
}
validatePidFile(pidFile);
}
return new ServerArgs(daemonize, quiet, pidFile, secrets, env.settings(), env.configFile(), env.logsFile());
}
@Override
public void close() {
if (server != null) {
server.stop();
}
}
// allow subclasses to access the started process
protected ServerProcess getServer() {
return server;
}
// protected to allow tests to override
protected Command loadTool(String toolname, String libs) {
return CliToolProvider.load(toolname, libs).create();
}
// protected to allow tests to override
protected ServerProcess startServer(Terminal terminal, ProcessInfo processInfo, ServerArgs args) throws Exception {
var tempDir = ServerProcessUtils.setupTempDir(processInfo);
var jvmOptions = JvmOptionsParser.determineJvmOptions(args, processInfo, tempDir, new MachineDependentHeap());
var serverProcessBuilder = new ServerProcessBuilder().withTerminal(terminal)
.withProcessInfo(processInfo)
.withServerArgs(args)
.withTempDir(tempDir)
.withJvmOptions(jvmOptions);
return serverProcessBuilder.start();
}
// protected to allow tests to override
protected SecureSettingsLoader secureSettingsLoader(Environment env) {
// TODO: Use the environment configuration to decide what kind of secrets store to load
return new KeyStoreLoader();
}
}