Skip to content

Commit 02b7b17

Browse files
authored
Prevent downgrades from 8.x to 7.x (#78586)
Users sometimes attempt to downgrade a node in place, but downgrades are totally untested and unsupported and generally don't work. We protect against this by recording the node version in the metadata and refusing to start if we encounter metadata written by a future version. However in 8.0 (#42489) we changed the directory layout so that a 7.x node won't find the upgraded metadata, or indeed any other data, and will proceed as if it's a fresh node. That's almost certainly not what the user wants, so with this commit we create a file at `${path.data}/nodes` at each startup, preventing an older node from starting. Closes #52414
1 parent 81aa483 commit 02b7b17

File tree

4 files changed

+51
-25
lines changed

4 files changed

+51
-25
lines changed

server/src/internalClusterTest/java/org/elasticsearch/env/NodeEnvironmentIT.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010

1111
import org.elasticsearch.Version;
1212
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
13-
import org.elasticsearch.core.CheckedConsumer;
14-
import org.elasticsearch.core.PathUtils;
1513
import org.elasticsearch.common.settings.Settings;
1614
import org.elasticsearch.common.xcontent.XContentType;
15+
import org.elasticsearch.core.CheckedConsumer;
16+
import org.elasticsearch.core.PathUtils;
1717
import org.elasticsearch.gateway.PersistedClusterStateService;
1818
import org.elasticsearch.indices.IndicesService;
1919
import org.elasticsearch.test.ESIntegTestCase;
@@ -138,8 +138,11 @@ public void testUpgradeDataFolder() throws IOException, InterruptedException {
138138
// simulate older data path layout by moving data under "nodes/0" folder
139139
final List<Path> dataPaths = List.of(PathUtils.get(Environment.PATH_DATA_SETTING.get(dataPathSettings)));
140140
dataPaths.forEach(path -> {
141-
final Path targetPath = path.resolve("nodes").resolve("0");
141+
final Path nodesPath = path.resolve("nodes");
142+
final Path targetPath = nodesPath.resolve("0");
142143
try {
144+
assertTrue(Files.isRegularFile(nodesPath));
145+
Files.delete(nodesPath);
143146
Files.createDirectories(targetPath);
144147

145148
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
@@ -192,9 +195,9 @@ public void testUpgradeDataFolder() throws IOException, InterruptedException {
192195
}
193196

194197
// check that upgrade works
195-
dataPaths.forEach(path -> assertTrue(Files.exists(path.resolve("nodes"))));
198+
dataPaths.forEach(path -> assertTrue(Files.isDirectory(path.resolve("nodes"))));
196199
internalCluster().startNode(dataPathSettings);
197-
dataPaths.forEach(path -> assertFalse(Files.exists(path.resolve("nodes"))));
200+
dataPaths.forEach(path -> assertTrue(Files.isRegularFile(path.resolve("nodes"))));
198201
assertEquals(nodeId, client().admin().cluster().prepareState().get().getState().nodes().getMasterNodeId());
199202
assertTrue(indexExists("test"));
200203
ensureYellow("test");

server/src/main/java/org/elasticsearch/env/NodeEnvironment.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,21 @@
2525
import org.elasticsearch.cluster.metadata.IndexMetadata;
2626
import org.elasticsearch.cluster.node.DiscoveryNode;
2727
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
28-
import org.elasticsearch.core.CheckedFunction;
29-
import org.elasticsearch.core.CheckedRunnable;
3028
import org.elasticsearch.common.Randomness;
31-
import org.elasticsearch.core.SuppressForbidden;
3229
import org.elasticsearch.common.UUIDs;
33-
import org.elasticsearch.core.Tuple;
3430
import org.elasticsearch.common.io.FileSystemUtils;
35-
import org.elasticsearch.core.Releasable;
3631
import org.elasticsearch.common.settings.Setting;
3732
import org.elasticsearch.common.settings.Setting.Property;
3833
import org.elasticsearch.common.settings.Settings;
3934
import org.elasticsearch.common.unit.ByteSizeValue;
40-
import org.elasticsearch.core.TimeValue;
4135
import org.elasticsearch.common.util.set.Sets;
4236
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
37+
import org.elasticsearch.core.CheckedFunction;
38+
import org.elasticsearch.core.CheckedRunnable;
39+
import org.elasticsearch.core.Releasable;
40+
import org.elasticsearch.core.SuppressForbidden;
41+
import org.elasticsearch.core.TimeValue;
42+
import org.elasticsearch.core.Tuple;
4343
import org.elasticsearch.core.internal.io.IOUtils;
4444
import org.elasticsearch.gateway.MetadataStateFormat;
4545
import org.elasticsearch.gateway.PersistedClusterStateService;
@@ -55,6 +55,7 @@
5555
import java.io.Closeable;
5656
import java.io.IOException;
5757
import java.io.UncheckedIOException;
58+
import java.nio.charset.StandardCharsets;
5859
import java.nio.file.AtomicMoveNotSupportedException;
5960
import java.nio.file.DirectoryStream;
6061
import java.nio.file.FileStore;
@@ -269,6 +270,16 @@ public NodeEnvironment(Settings settings, Environment environment) throws IOExce
269270
assertCanWrite();
270271
}
271272

273+
// versions 7.x and earlier put their data under ${path.data}/nodes/; leave a file at that location to prevent downgrades
274+
final Path legacyNodesPath = environment.dataFile().resolve("nodes");
275+
if (Files.isRegularFile(legacyNodesPath) == false) {
276+
final String content = "written by Elasticsearch v" + Version.CURRENT +
277+
" to prevent a downgrade to a version prior to v8.0.0 which would result in data loss";
278+
Files.write(legacyNodesPath, content.getBytes(StandardCharsets.UTF_8));
279+
IOUtils.fsync(legacyNodesPath, false);
280+
IOUtils.fsync(environment.dataFile(), true);
281+
}
282+
272283
if (DiscoveryNode.canContainData(settings) == false) {
273284
if (DiscoveryNode.isMasterNode(settings) == false) {
274285
ensureNoIndexMetadata(nodePath);
@@ -408,12 +419,11 @@ private static boolean upgradeLegacyNodeFolders(Logger logger, Settings settings
408419
IOUtils.fsync(nodePath.path, true);
409420
});
410421

411-
// now do the actual upgrade. start by upgrading the node metadata file before moving anything, since a downgrade in an
412-
// intermediate state would be pretty disastrous
413-
loadNodeMetadata(settings, logger, legacyNodeLock.getNodePath());
422+
// now do the actual upgrade
414423
for (CheckedRunnable<IOException> upgradeAction : upgradeActions) {
415424
upgradeAction.run();
416425
}
426+
417427
} finally {
418428
legacyNodeLock.close();
419429
}

server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.elasticsearch.test.NodeRoles;
2727

2828
import java.io.IOException;
29+
import java.nio.charset.StandardCharsets;
2930
import java.nio.file.Files;
3031
import java.nio.file.Path;
3132
import java.util.ArrayList;
@@ -41,6 +42,7 @@
4142
import static org.elasticsearch.test.NodeRoles.nonDataNode;
4243
import static org.elasticsearch.test.NodeRoles.nonMasterNode;
4344
import static org.hamcrest.CoreMatchers.equalTo;
45+
import static org.hamcrest.Matchers.allOf;
4446
import static org.hamcrest.Matchers.containsString;
4547
import static org.hamcrest.Matchers.empty;
4648
import static org.hamcrest.Matchers.startsWith;
@@ -439,6 +441,23 @@ public void testEnsureNoShardDataOrIndexMetadata() throws IOException {
439441
verifyFailsOnShardData(noDataNoMasterSettings, indexPath, shardDataDirName);
440442
}
441443

444+
public void testBlocksDowngradeToVersionWithMultipleNodesInDataPath() throws IOException {
445+
final Settings settings = buildEnvSettings(Settings.EMPTY);
446+
for (int i = 0; i < 2; i++) { // ensure the file gets created again if missing
447+
try (NodeEnvironment env = newNodeEnvironment(settings)) {
448+
final Path nodesPath = env.nodeDataPath().resolve("nodes");
449+
assertTrue(Files.isRegularFile(nodesPath));
450+
assertThat(
451+
Files.readString(nodesPath, StandardCharsets.UTF_8),
452+
allOf(
453+
containsString("written by Elasticsearch"),
454+
containsString("prevent a downgrade"),
455+
containsString("data loss")));
456+
Files.delete(nodesPath);
457+
}
458+
}
459+
}
460+
442461
private void verifyFailsOnShardData(Settings settings, Path indexPath, String shardDataDirName) {
443462
IllegalStateException ex = expectThrows(IllegalStateException.class,
444463
"Must fail creating NodeEnvironment on a data path that has shard data if node does not have data role",
@@ -459,17 +478,6 @@ private void verifyFailsOnMetadata(Settings settings, Path indexPath) {
459478
assertThat(ex.getMessage(), startsWith("node does not have the data and master roles but has index metadata"));
460479
}
461480

462-
/**
463-
* Converts an array of Strings to an array of Paths, adding an additional child if specified
464-
*/
465-
private Path[] stringsToPaths(String[] strings, String additional) {
466-
Path[] locations = new Path[strings.length];
467-
for (int i = 0; i < strings.length; i++) {
468-
locations[i] = PathUtils.get(strings[i], additional);
469-
}
470-
return locations;
471-
}
472-
473481
@Override
474482
public NodeEnvironment newNodeEnvironment() throws IOException {
475483
return newNodeEnvironment(Settings.EMPTY);

test/framework/src/main/java/org/elasticsearch/cluster/DiskUsageIntegTestCase.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import java.io.FileNotFoundException;
2828
import java.io.IOException;
29+
import java.nio.charset.StandardCharsets;
2930
import java.nio.file.DirectoryStream;
3031
import java.nio.file.FileStore;
3132
import java.nio.file.FileSystem;
@@ -148,6 +149,10 @@ public long getUnallocatedSpace() throws IOException {
148149

149150
private static long getTotalFileSize(Path path) throws IOException {
150151
if (Files.isRegularFile(path)) {
152+
if (path.getFileName().toString().equals("nodes")
153+
&& Files.readString(path, StandardCharsets.UTF_8).contains("prevent a downgrade")) {
154+
return 0;
155+
}
151156
try {
152157
return Files.size(path);
153158
} catch (NoSuchFileException | FileNotFoundException e) {

0 commit comments

Comments
 (0)