Skip to content

Commit 4906a3c

Browse files
authored
A CLI tool to assist during an upgrade to OpenSearch. (#846)
This change adds the initial version of a new CLI tool `opensearch-upgrade` as part of the OpenSearch distribution. This tool is meant for assisting during an upgrade from an existing Elasticsearch v7.10.2/v6.8.0 node to OpenSearch. It automates the process of importing existing configurations and installing of core plugins. Signed-off-by: Rabi Panda <[email protected]>
1 parent c7617b0 commit 4906a3c

32 files changed

+1685
-6
lines changed

distribution/build.gradle

+5-1
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
296296
* Properties to expand when copying packaging files *
297297
*****************************************************************************/
298298
configurations {
299-
['libs', 'libsPluginCli', 'libsKeystoreCli'].each {
299+
['libs', 'libsPluginCli', 'libsKeystoreCli', 'libsUpgradeCli'].each {
300300
create(it) {
301301
canBeConsumed = false
302302
canBeResolved = true
@@ -317,6 +317,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
317317

318318
libsPluginCli project(':distribution:tools:plugin-cli')
319319
libsKeystoreCli project(path: ':distribution:tools:keystore-cli')
320+
libsUpgradeCli project(path: ':distribution:tools:upgrade-cli')
320321
}
321322

322323
project.ext {
@@ -334,6 +335,9 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
334335
into('tools/keystore-cli') {
335336
from(configurations.libsKeystoreCli)
336337
}
338+
into('tools/upgrade-cli') {
339+
from(configurations.libsUpgradeCli)
340+
}
337341
}
338342
}
339343

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
OPENSEARCH_MAIN_CLASS=org.opensearch.upgrade.UpgradeCli \
4+
OPENSEARCH_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/upgrade-cli \
5+
"`dirname "$0"`"/opensearch-cli \
6+
"$@"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@echo off
2+
3+
setlocal enabledelayedexpansion
4+
setlocal enableextensions
5+
6+
set OPENSEARCH_MAIN_CLASS=org.opensearch.upgrade.UpgradeCli
7+
set OPENSEARCH_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/upgrade-cli
8+
call "%~dp0opensearch-cli.bat" ^
9+
%%* ^
10+
|| goto exit
11+
12+
13+
endlocal
14+
endlocal
15+
:exit
16+
exit /b %ERRORLEVEL%
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
apply plugin: 'opensearch.build'
11+
12+
archivesBaseName = 'opensearch-upgrade-cli'
13+
14+
dependencies {
15+
compileOnly project(":server")
16+
compileOnly project(":libs:opensearch-cli")
17+
implementation "com.fasterxml.jackson.core:jackson-core:${versions.jackson}"
18+
implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
19+
implementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}"
20+
testImplementation project(":test:framework")
21+
testImplementation 'com.google.jimfs:jimfs:1.2'
22+
testRuntimeOnly 'com.google.guava:guava:30.1.1-jre'
23+
}
24+
25+
tasks.named("dependencyLicenses").configure {
26+
mapping from: /jackson-.*/, to: 'jackson'
27+
}
28+
29+
test {
30+
systemProperty 'tests.security.manager', 'false'
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
This copy of Jackson JSON processor streaming parser/generator is licensed under the
2+
Apache (Software) License, version 2.0 ("the License").
3+
See the License for details about distribution rights, and the
4+
specific rights regarding derivate works.
5+
6+
You may obtain a copy of the License at:
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Jackson JSON processor
2+
3+
Jackson is a high-performance, Free/Open Source JSON processing library.
4+
It was originally written by Tatu Saloranta ([email protected]), and has
5+
been in development since 2007.
6+
It is currently developed by a community of developers, as well as supported
7+
commercially by FasterXML.com.
8+
9+
## Licensing
10+
11+
Jackson core and extension components may licensed under different licenses.
12+
To find the details that apply to this artifact see the accompanying LICENSE file.
13+
For more information, including possible other licensing options, contact
14+
FasterXML.com (http://fasterxml.com).
15+
16+
## Credits
17+
18+
A list of contributors may be found from CREDITS file, which is included
19+
in some artifacts (usually source distributions); but is always available
20+
from the source code management (SCM) system project uses.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2c3f5c079330f3a01726686a078979420f547ae4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
5d9f3d441f99d721b957e3497f0a6465c764fad4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
package org.opensearch.common.settings;
10+
11+
/**
12+
* Utility that has package level access to the {@link KeyStoreWrapper} for
13+
* saving a setting.
14+
*/
15+
public final class KeystoreWrapperUtil {
16+
/**
17+
* No public constructor. Contains only static functions.
18+
*/
19+
private KeystoreWrapperUtil() {}
20+
21+
/**
22+
* Save a secure setting using the wrapper.
23+
*
24+
* @param keystore an instance of {@link KeyStoreWrapper}
25+
* @param setting setting to save
26+
* @param bytes value of the setting in bytes
27+
*/
28+
public static void saveSetting(KeyStoreWrapper keystore, String setting, byte[] bytes) {
29+
keystore.setFile(setting, bytes);
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
* This exists to get access to the package level methods of KeyStoreWrapper.
11+
*/
12+
package org.opensearch.common.settings;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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+
package org.opensearch.upgrade;
10+
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
import org.opensearch.Version;
13+
import org.opensearch.cli.Terminal;
14+
import org.opensearch.common.SuppressForbidden;
15+
import org.opensearch.common.collect.Tuple;
16+
import org.opensearch.common.settings.Settings;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.net.HttpURLConnection;
21+
import java.net.URL;
22+
import java.nio.file.Path;
23+
import java.util.ArrayList;
24+
import java.util.Collections;
25+
import java.util.HashMap;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Optional;
29+
import java.util.Scanner;
30+
31+
/**
32+
* Looks for an existing elasticsearch installation. First it tries to identify automatically,
33+
* and if unsuccessful, asks the user to input the missing details.
34+
* <p>
35+
* If an elasticsearch installation can not be found, throws a runtime error which fails the
36+
* upgrade task.
37+
*/
38+
class DetectEsInstallationTask implements UpgradeTask {
39+
private static final int ES_DEFAULT_PORT = 9200;
40+
private static final String ES_CONFIG_ENV = "ES_PATH_CONF";
41+
private static final String ES_CONFIG_YML = "elasticsearch.yml";
42+
private static final String ES_HOME = "ES_HOME";
43+
44+
@SuppressForbidden(reason = "We need to read external es config files")
45+
@Override
46+
public void accept(final Tuple<TaskInput, Terminal> input) {
47+
final TaskInput taskInput = input.v1();
48+
final Terminal terminal = input.v2();
49+
try {
50+
terminal.println("Looking for an elasticsearch installation ...");
51+
String esHomeEnv = System.getenv(ES_HOME);
52+
if (esHomeEnv == null) {
53+
esHomeEnv = terminal.readText("Missing ES_HOME env variable, enter the path to elasticsearch home: ");
54+
if (esHomeEnv == null || esHomeEnv.isEmpty()) {
55+
throw new RuntimeException("Invalid input for path to elasticsearch home directory.");
56+
}
57+
}
58+
taskInput.setEsHome(new File(esHomeEnv).toPath());
59+
60+
String esConfEnv = System.getenv(ES_CONFIG_ENV);
61+
if (esConfEnv == null) {
62+
esConfEnv = terminal.readText("Missing ES_PATH_CONF env variable, enter the path to elasticsearch config directory: ");
63+
if (esConfEnv == null || esHomeEnv.isEmpty()) {
64+
throw new RuntimeException("Invalid input for path to elasticsearch config directory.");
65+
}
66+
}
67+
taskInput.setEsConfig(new File(esConfEnv).toPath());
68+
69+
final Settings esSettings = Settings.builder().loadFromPath(taskInput.getEsConfig().resolve(ES_CONFIG_YML)).build();
70+
final String url = retrieveUrl(esSettings);
71+
taskInput.setBaseUrl(url);
72+
final boolean running = isRunning(url);
73+
taskInput.setRunning(running);
74+
if (running) {
75+
terminal.println("Found a running instance of elasticsearch at " + url);
76+
taskInput.setRunning(true);
77+
try {
78+
updateTaskInput(taskInput, fetchInfoFromUrl(taskInput.getBaseUrl()));
79+
} catch (RuntimeException e) {
80+
updateTaskInput(taskInput, fetchInfoFromEsSettings(esSettings));
81+
}
82+
try {
83+
taskInput.setPlugins(fetchPluginsFromUrl(taskInput.getBaseUrl()));
84+
} catch (RuntimeException e) {
85+
taskInput.setPlugins(detectPluginsFromEsHome(taskInput.getEsHome()));
86+
}
87+
} else {
88+
terminal.println("Did not find a running instance of elasticsearch at " + url);
89+
updateTaskInput(taskInput, fetchInfoFromEsSettings(esSettings));
90+
taskInput.setPlugins(detectPluginsFromEsHome(taskInput.getEsHome()));
91+
}
92+
} catch (IOException e) {
93+
throw new RuntimeException("Error detecting existing elasticsearch installation. " + e);
94+
}
95+
}
96+
97+
@SuppressWarnings("unchecked")
98+
private void updateTaskInput(TaskInput taskInput, Map<?, ?> response) {
99+
final Map<String, String> versionMap = (Map<String, String>) response.get("version");
100+
if (versionMap != null) {
101+
final String vStr = versionMap.get("number");
102+
if (vStr != null) {
103+
taskInput.setVersion(Version.fromString(vStr));
104+
}
105+
}
106+
taskInput.setNode((String) response.get("name"));
107+
taskInput.setCluster((String) response.get("cluster_name"));
108+
}
109+
110+
// package private for unit testing
111+
String retrieveUrl(final Settings esSettings) {
112+
final int port = Optional.ofNullable(esSettings.get("http.port")).map(this::extractPort).orElse(ES_DEFAULT_PORT);
113+
return "http://localhost:" + port;
114+
}
115+
116+
private Integer extractPort(final String port) {
117+
try {
118+
return Integer.parseInt(port.trim());
119+
} catch (Exception ex) {
120+
return ES_DEFAULT_PORT;
121+
}
122+
}
123+
124+
@SuppressForbidden(reason = "Need to connect to http endpoint for elasticsearch.")
125+
private boolean isRunning(final String url) {
126+
try {
127+
final URL esUrl = new URL(url);
128+
final HttpURLConnection conn = (HttpURLConnection) esUrl.openConnection();
129+
conn.setRequestMethod("GET");
130+
conn.setConnectTimeout(1000);
131+
conn.connect();
132+
return conn.getResponseCode() == 200;
133+
} catch (IOException e) {
134+
return false;
135+
}
136+
}
137+
138+
@SuppressForbidden(reason = "Retrieve information on the installation.")
139+
private Map<?, ?> fetchInfoFromUrl(final String url) {
140+
try {
141+
final URL esUrl = new URL(url);
142+
final HttpURLConnection conn = (HttpURLConnection) esUrl.openConnection();
143+
conn.setRequestMethod("GET");
144+
conn.setConnectTimeout(1000);
145+
conn.connect();
146+
147+
final StringBuilder json = new StringBuilder();
148+
final Scanner scanner = new Scanner(esUrl.openStream());
149+
while (scanner.hasNext()) {
150+
json.append(scanner.nextLine());
151+
}
152+
scanner.close();
153+
final ObjectMapper mapper = new ObjectMapper();
154+
return mapper.readValue(json.toString(), Map.class);
155+
} catch (IOException e) {
156+
throw new RuntimeException("Error retrieving elasticsearch cluster info, " + e);
157+
}
158+
}
159+
160+
private Map<?, ?> fetchInfoFromEsSettings(final Settings esSettings) throws IOException {
161+
final Map<String, String> info = new HashMap<>();
162+
final String node = esSettings.get("node.name") != null ? esSettings.get("node.name") : "unknown";
163+
final String cluster = esSettings.get("cluster.name") != null ? esSettings.get("cluster.name") : "unknown";
164+
info.put("name", node);
165+
info.put("cluster_name", cluster);
166+
return info;
167+
}
168+
169+
@SuppressWarnings("unchecked")
170+
@SuppressForbidden(reason = "Retrieve information on installed plugins.")
171+
private List<String> fetchPluginsFromUrl(final String url) {
172+
final List<String> plugins = new ArrayList<>();
173+
try {
174+
final URL esUrl = new URL(url + "/_cat/plugins?format=json&local=true");
175+
final HttpURLConnection conn = (HttpURLConnection) esUrl.openConnection();
176+
conn.setRequestMethod("GET");
177+
conn.setConnectTimeout(1000);
178+
conn.connect();
179+
if (conn.getResponseCode() == 200) {
180+
final StringBuilder json = new StringBuilder();
181+
final Scanner scanner = new Scanner(esUrl.openStream());
182+
while (scanner.hasNext()) {
183+
json.append(scanner.nextLine());
184+
}
185+
scanner.close();
186+
final ObjectMapper mapper = new ObjectMapper();
187+
final Map<String, String>[] response = mapper.readValue(json.toString(), Map[].class);
188+
for (Map<String, String> plugin : response) {
189+
plugins.add(plugin.get("component"));
190+
}
191+
}
192+
return plugins;
193+
} catch (IOException e) {
194+
throw new RuntimeException("Error retrieving elasticsearch plugin details, " + e);
195+
}
196+
}
197+
198+
private List<String> detectPluginsFromEsHome(final Path esHome) {
199+
// list out the contents of the plugins directory under esHome
200+
return Collections.emptyList();
201+
}
202+
}

0 commit comments

Comments
 (0)