aboutsummaryrefslogtreecommitdiffstats
path: root/node-admin
diff options
context:
space:
mode:
authorValerij Fredriksen <valerijf@verizonmedia.com>2021-05-04 17:42:49 +0200
committerValerij Fredriksen <valerijf@verizonmedia.com>2021-05-04 17:42:49 +0200
commit2a57da593db86f0df158aaa27f1944fe6a357edd (patch)
treecc4696d69b400d4aff52fcc59cf3d44175317e9d /node-admin
parent448d9205aa15351fbb770a7fbfae88d5b33bbe3d (diff)
Skip invoking yum on install/remove if package(s) are present/missing in rpm
Diffstat (limited to 'node-admin')
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/Yum.java42
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumCommand.java75
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTester.java52
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTest.java25
4 files changed, 126 insertions, 68 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/Yum.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/Yum.java
index 482f324a9b5..d8a131f5ed1 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/Yum.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/Yum.java
@@ -2,16 +2,10 @@
package com.yahoo.vespa.hosted.node.admin.task.util.yum;
import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
-import com.yahoo.vespa.hosted.node.admin.task.util.process.CommandResult;
import com.yahoo.vespa.hosted.node.admin.task.util.process.Terminal;
import java.util.List;
import java.util.Optional;
-import java.util.function.Function;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
import static com.yahoo.vespa.hosted.node.admin.task.util.yum.YumCommand.GenericYumCommand;
import static com.yahoo.vespa.hosted.node.admin.task.util.yum.YumCommand.InstallFixedYumCommand;
@@ -21,20 +15,6 @@ import static com.yahoo.vespa.hosted.node.admin.task.util.yum.YumCommand.Install
*/
public class Yum {
- // Note: "(?dm)" makes newline be \n (only), and enables multiline mode where ^$ match lines with find()
- public static final Pattern INSTALL_NOOP_PATTERN = Pattern.compile("(?dm)^Nothing to do\\.?$");
- public static final Pattern UPGRADE_NOOP_PATTERN = Pattern.compile("(?dm)^No packages marked for update$");
- public static final Pattern REMOVE_NOOP_PATTERN = Pattern.compile("(?dm)^No [pP]ackages marked for removal\\.?$");
-
- // WARNING: These must be in the same order as the supplier below
- private static final String RPM_QUERYFORMAT = Stream.of("NAME", "EPOCH", "VERSION", "RELEASE", "ARCH")
- .map(formatter -> "%{" + formatter + "}")
- .collect(Collectors.joining("\\n"));
- private static final Function<YumPackageName.Builder, List<Function<String, YumPackageName.Builder>>>
- PACKAGE_NAME_BUILDERS_GENERATOR = builder -> List.of(
- builder::setName, builder::setEpoch, builder::setVersion, builder::setRelease, builder::setArchitecture);
-
-
private final Terminal terminal;
public Yum(Terminal terminal) {
@@ -42,21 +22,7 @@ public class Yum {
}
public Optional<YumPackageName> queryInstalled(TaskContext context, String packageName) {
- CommandResult commandResult = terminal.newCommandLine(context)
- .add("rpm", "-q", packageName, "--queryformat", RPM_QUERYFORMAT)
- .ignoreExitCode()
- .executeSilently();
-
- if (commandResult.getExitCode() != 0) return Optional.empty();
-
- YumPackageName.Builder builder = new YumPackageName.Builder();
- List<Function<String, YumPackageName.Builder>> builders = PACKAGE_NAME_BUILDERS_GENERATOR.apply(builder);
- List<Optional<String>> lines = commandResult.mapEachLine(line -> Optional.of(line).filter(s -> !"(none)".equals(s)));
- if (lines.size() != builders.size()) throw new IllegalStateException(String.format(
- "Unexpected response from rpm, expected %d lines, got %s", builders.size(), commandResult.getOutput()));
-
- IntStream.range(0, builders.size()).forEach(i -> lines.get(i).ifPresent(builders.get(i)::apply));
- return Optional.of(builder.build());
+ return YumCommand.queryInstalled(terminal, context, packageName);
}
/** Lock and install, or if necessary downgrade, a package to a given version. */
@@ -65,7 +31,7 @@ public class Yum {
}
public GenericYumCommand install(YumPackageName... packages) {
- return new GenericYumCommand(terminal, "install", List.of(packages), INSTALL_NOOP_PATTERN);
+ return new GenericYumCommand(terminal, GenericYumCommand.CommandType.install, List.of(packages));
}
public GenericYumCommand install(String package1, String... packages) {
@@ -78,7 +44,7 @@ public class Yum {
public GenericYumCommand upgrade(YumPackageName... packages) {
- return new GenericYumCommand(terminal, "upgrade", List.of(packages), INSTALL_NOOP_PATTERN, UPGRADE_NOOP_PATTERN);
+ return new GenericYumCommand(terminal, GenericYumCommand.CommandType.upgrade, List.of(packages));
}
public GenericYumCommand upgrade(String package1, String... packages) {
@@ -91,7 +57,7 @@ public class Yum {
public GenericYumCommand remove(YumPackageName... packages) {
- return new GenericYumCommand(terminal, "remove", List.of(packages), REMOVE_NOOP_PATTERN);
+ return new GenericYumCommand(terminal, GenericYumCommand.CommandType.remove, List.of(packages));
}
public GenericYumCommand remove(String package1, String... packages) {
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumCommand.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumCommand.java
index f7ca453256f..2a01a5ebcb4 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumCommand.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumCommand.java
@@ -4,20 +4,37 @@ package com.yahoo.vespa.hosted.node.admin.task.util.yum;
import com.yahoo.component.Version;
import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
import com.yahoo.vespa.hosted.node.admin.task.util.process.CommandLine;
+import com.yahoo.vespa.hosted.node.admin.task.util.process.CommandResult;
import com.yahoo.vespa.hosted.node.admin.task.util.process.Terminal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
/**
* @author freva
*/
public abstract class YumCommand<T extends YumCommand<T>> {
+ // Note: "(?dm)" makes newline be \n (only), and enables multiline mode where ^$ match lines with find()
+ public static final Pattern INSTALL_NOOP_PATTERN = Pattern.compile("(?dm)^Nothing to do\\.?$");
+ public static final Pattern UPGRADE_NOOP_PATTERN = Pattern.compile("(?dm)^No packages marked for update$");
+ public static final Pattern REMOVE_NOOP_PATTERN = Pattern.compile("(?dm)^No [pP]ackages marked for removal\\.?$");
+
+ // WARNING: These must be in the same order as the supplier below
+ private static final String RPM_QUERYFORMAT = Stream.of("NAME", "EPOCH", "VERSION", "RELEASE", "ARCH")
+ .map(formatter -> "%{" + formatter + "}")
+ .collect(Collectors.joining("\\n"));
+ private static final Function<YumPackageName.Builder, List<Function<String, YumPackageName.Builder>>>
+ PACKAGE_NAME_BUILDERS_GENERATOR = builder -> List.of(
+ builder::setName, builder::setEpoch, builder::setVersion, builder::setRelease, builder::setArchitecture);
+
private List<String> enabledRepos = List.of();
private final Terminal terminal;
@@ -54,34 +71,31 @@ public abstract class YumCommand<T extends YumCommand<T>> {
private static final Pattern UNKNOWN_PACKAGE_PATTERN = Pattern.compile("(?dm)^No package ([^ ]+) available\\.$");
private final Terminal terminal;
- private final String yumCommand;
- private final List<Pattern> outputNoopPatterns;
+ private final CommandType yumCommand;
private final List<YumPackageName> packages;
private final List<String> options = new ArrayList<>();
- GenericYumCommand(Terminal terminal, String yumCommand, List<YumPackageName> packages, Pattern... outputNoopPatterns) {
+ GenericYumCommand(Terminal terminal, CommandType yumCommand, List<YumPackageName> packages) {
super(terminal);
this.terminal = terminal;
this.yumCommand = yumCommand;
this.packages = packages;
- this.outputNoopPatterns = List.of(outputNoopPatterns);
switch (yumCommand) {
- case "install": {
+ case install: {
if (packages.size() > 1) options.add("skip_missing_names_on_install=False");
break;
}
- case "upgrade": {
+ case upgrade: {
if (packages.size() > 1) options.add("skip_missing_names_on_update=False");
break;
}
- case "remove": break;
+ case remove: break;
default: throw new IllegalArgumentException("Unknown yum command: " + yumCommand);
}
- if (packages.isEmpty() && ! "upgrade".equals(yumCommand)) {
+ if (packages.isEmpty() && yumCommand != CommandType.upgrade)
throw new IllegalArgumentException("No packages specified");
- }
}
@Override
@@ -92,13 +106,14 @@ public abstract class YumCommand<T extends YumCommand<T>> {
@Override
public boolean converge(TaskContext context) {
- if (packages.isEmpty() && ! "upgrade".equals(yumCommand)) {
- throw new IllegalArgumentException("No packages specified");
- }
+ if (yumCommand == CommandType.install)
+ if (packages.stream().allMatch(pkg -> isInstalled(context, pkg))) return false;
+ if (yumCommand == CommandType.remove)
+ if (packages.stream().noneMatch(pkg -> isInstalled(context, pkg))) return false;
Version yumVersion = version(context);
CommandLine commandLine = terminal.newCommandLine(context);
- commandLine.add("yum", yumCommand);
+ commandLine.add("yum", yumCommand.name());
addParametersToCommandLine(commandLine);
commandLine.add(packages.stream().map(pkg -> pkg.toName(yumVersion)).collect(Collectors.toList()));
@@ -121,10 +136,19 @@ public abstract class YumCommand<T extends YumCommand<T>> {
throw new IllegalArgumentException("Unknown package: " + unknownPackageMatcher.group(1));
}
- return outputNoopPatterns.stream().noneMatch(pattern -> pattern.matcher(output).find());
+ return yumCommand.outputNoopPatterns.stream().noneMatch(pattern -> pattern.matcher(output).find());
}
protected GenericYumCommand getThis() { return this; }
+
+ enum CommandType {
+ install(INSTALL_NOOP_PATTERN), remove(REMOVE_NOOP_PATTERN), upgrade(INSTALL_NOOP_PATTERN, UPGRADE_NOOP_PATTERN);
+
+ private final List<Pattern> outputNoopPatterns;
+ CommandType(Pattern... outputNoopPatterns) {
+ this.outputNoopPatterns = List.of(outputNoopPatterns);
+ }
+ }
}
@@ -211,7 +235,7 @@ public abstract class YumCommand<T extends YumCommand<T>> {
String output = installCommand.executeSilently().getUntrimmedOutput();
- if (Yum.INSTALL_NOOP_PATTERN.matcher(output).find()) {
+ if (INSTALL_NOOP_PATTERN.matcher(output).find()) {
if (CHECKING_FOR_UPDATE_PATTERN.matcher(output).find()) {
// case 3.
var upgradeCommand = terminal.newCommandLine(context).add("yum", "downgrade");
@@ -233,4 +257,25 @@ public abstract class YumCommand<T extends YumCommand<T>> {
protected InstallFixedYumCommand getThis() { return this; }
}
+ protected boolean isInstalled(TaskContext context, YumPackageName yumPackage) {
+ return queryInstalled(terminal, context, yumPackage.getName()).map(yumPackage::isSubsetOf).orElse(false);
+ }
+
+ static Optional<YumPackageName> queryInstalled(Terminal terminal, TaskContext context, String packageName) {
+ CommandResult commandResult = terminal.newCommandLine(context)
+ .add("rpm", "-q", packageName, "--queryformat", RPM_QUERYFORMAT)
+ .ignoreExitCode()
+ .executeSilently();
+
+ if (commandResult.getExitCode() != 0) return Optional.empty();
+
+ YumPackageName.Builder builder = new YumPackageName.Builder();
+ List<Function<String, YumPackageName.Builder>> builders = PACKAGE_NAME_BUILDERS_GENERATOR.apply(builder);
+ List<Optional<String>> lines = commandResult.mapEachLine(line -> Optional.of(line).filter(s -> !"(none)".equals(s)));
+ if (lines.size() != builders.size()) throw new IllegalStateException(String.format(
+ "Unexpected response from rpm, expected %d lines, got %s", builders.size(), commandResult.getOutput()));
+
+ IntStream.range(0, builders.size()).forEach(i -> lines.get(i).ifPresent(builders.get(i)::apply));
+ return Optional.of(builder.build());
+ }
}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTester.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTester.java
index 589362e747f..e47d71cbdf7 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTester.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTester.java
@@ -34,15 +34,15 @@ public class YumTester extends Yum {
}
public GenericYumCommandExpectation expectInstall(String... packages) {
- return new GenericYumCommandExpectation("install", packages);
+ return new GenericYumCommandExpectation(CommandType.install, packages);
}
public GenericYumCommandExpectation expectUpdate(String... packages) {
- return new GenericYumCommandExpectation("upgrade", packages);
+ return new GenericYumCommandExpectation(CommandType.upgrade, packages);
}
public GenericYumCommandExpectation expectRemove(String... packages) {
- return new GenericYumCommandExpectation("remove", packages);
+ return new GenericYumCommandExpectation(CommandType.remove, packages);
}
public InstallFixedCommandExpectation expectInstallFixedVersion(String yumPackage) {
@@ -55,12 +55,12 @@ public class YumTester extends Yum {
public class GenericYumCommandExpectation {
- private final String command;
+ private final CommandType commandType;
protected final List<YumPackageName> packages;
private List<String> enableRepos = List.of();
- private GenericYumCommandExpectation(String command, String... packages) {
- this.command = command;
+ private GenericYumCommandExpectation(CommandType commandType, String... packages) {
+ this.commandType = commandType;
this.packages = Stream.of(packages).map(YumPackageName::fromString).collect(Collectors.toList());
}
@@ -72,11 +72,12 @@ public class YumTester extends Yum {
/** Mock the return value of the converge(TaskContext) method for this operation (true iff system was modified) */
public YumTester andReturn(boolean value) {
if (value) return execute("Success");
- switch (command) {
- case "install": return execute("Nothing to do");
- case "upgrade": return execute("No packages marked for update");
- case "remove": return execute("No Packages marked for removal");
- default: throw new IllegalArgumentException("Unknown command: " + command);
+ switch (commandType) {
+ case installFixed:
+ case install: return execute("Nothing to do");
+ case upgrade: return execute("No packages marked for update");
+ case remove: return execute("No Packages marked for removal");
+ default: throw new IllegalArgumentException("Unknown command type: " + commandType);
}
}
@@ -85,12 +86,24 @@ public class YumTester extends Yum {
}
private YumTester execute(String output) {
+ if (commandType == CommandType.install)
+ terminal.interceptCommand("rpm query", cmd -> new TestChildProcess2(1, "Not installed"));
+ if (commandType == CommandType.remove) { // Pretend the first package is installed so we can continue to yum commands
+ YumPackageName pkg = packages.get(0);
+ terminal.interceptCommand("rpm query", cmd -> new TestChildProcess2(0, String.join("\n",
+ pkg.getName(),
+ pkg.getEpoch().orElse("(none)"),
+ pkg.getVersion().orElse("1.2.3"),
+ pkg.getRelease().orElse("1"),
+ pkg.getArchitecture().orElse("(none)"))));
+ }
+
StringBuilder cmd = new StringBuilder();
- cmd.append("yum ").append(command).append(" --assumeyes");
+ cmd.append("yum ").append(commandType.command).append(" --assumeyes");
enableRepos.forEach(repo -> cmd.append(" --enablerepo=").append(repo));
- if ("install".equals(command) && packages.size() > 1)
+ if (commandType == CommandType.install && packages.size() > 1)
cmd.append(" --setopt skip_missing_names_on_install=False");
- if ("upgrade".equals(command) && packages.size() > 1)
+ if (commandType == CommandType.upgrade && packages.size() > 1)
cmd.append(" --setopt skip_missing_names_on_update=False");
packages.forEach(pkg -> {
String name = pkg.toName(yumVersion);
@@ -109,7 +122,7 @@ public class YumTester extends Yum {
public class InstallFixedCommandExpectation extends GenericYumCommandExpectation {
private InstallFixedCommandExpectation(String yumPackage) {
- super("install", yumPackage);
+ super(CommandType.installFixed, yumPackage);
}
@Override
@@ -150,4 +163,13 @@ public class YumTester extends Yum {
}
}
+ private enum CommandType {
+ install("install"), upgrade("upgrade"), remove("remove"), installFixed("install");
+
+ private final String command;
+ CommandType(String command) {
+ this.command = command;
+ }
+ }
+
}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTest.java
index 37695ca9504..92f8f78d255 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTest.java
@@ -78,6 +78,7 @@ public class YumTest {
@Test
public void testAlreadyInstalled() {
+ mockRpmQuery("package-1", null);
mockYumVersion();
terminal.expectCommand(
"yum install --assumeyes --enablerepo=repo1 --enablerepo=repo2 --setopt skip_missing_names_on_install=False package-1 package-2 2>&1",
@@ -90,6 +91,7 @@ public class YumTest {
.converge(taskContext));
// RHEL 8
+ mockRpmQuery("package-1", null);
mockYumVersion(YumVersion.rhel8);
terminal.expectCommand(
"yum install --assumeyes --enablerepo=repo1 --enablerepo=repo2 --setopt skip_missing_names_on_install=False package-1 package-2 2>&1",
@@ -125,6 +127,7 @@ public class YumTest {
@Test
public void testAlreadyRemoved() {
+ mockRpmQuery("package-1", YumPackageName.fromString("package-1-1.2.3-1"));
mockYumVersion();
terminal.expectCommand(
"yum remove --assumeyes package-1 package-2 2>&1",
@@ -136,6 +139,7 @@ public class YumTest {
.converge(taskContext));
// RHEL 8
+ mockRpmQuery("package-1", YumPackageName.fromString("package-1-1.2.3-1"));
mockYumVersion(YumVersion.rhel8);
terminal.expectCommand(
"yum remove --assumeyes package-1 package-2 2>&1",
@@ -147,7 +151,15 @@ public class YumTest {
}
@Test
+ public void skipsYumRemoveNotInRpm() {
+ mockRpmQuery("package-1", null);
+ mockRpmQuery("package-2", null);
+ assertFalse(yum.remove("package-1", "package-2").converge(taskContext));
+ }
+
+ @Test
public void testInstall() {
+ mockRpmQuery("package-1", null);
mockYumVersion();
terminal.expectCommand(
"yum install --assumeyes --setopt skip_missing_names_on_install=False package-1 package-2 2>&1",
@@ -160,7 +172,15 @@ public class YumTest {
}
@Test
+ public void skipsYumInstallIfInRpm() {
+ mockRpmQuery("package-1", YumPackageName.fromString("package-1-1.2.3-1"));
+ mockRpmQuery("package-2", YumPackageName.fromString("1:package-2-1.2.3-1.el7.x86_64"));
+ assertFalse(yum.install("package-1-1.2.3-1", "package-2").converge(taskContext));
+ }
+
+ @Test
public void testInstallWithEnablerepo() {
+ mockRpmQuery("package-1", null);
mockYumVersion();
terminal.expectCommand(
"yum install --assumeyes --enablerepo=repo-name --setopt skip_missing_names_on_install=False package-1 package-2 2>&1",
@@ -273,6 +293,7 @@ public class YumTest {
@Test(expected = ChildProcessFailureException.class)
public void testFailedInstall() {
+ mockRpmQuery("package-1", null);
mockYumVersion();
terminal.expectCommand(
"yum install --assumeyes --enablerepo=repo-name --setopt skip_missing_names_on_install=False package-1 package-2 2>&1",
@@ -288,6 +309,7 @@ public class YumTest {
@Test
public void testUnknownPackages() {
+ mockRpmQuery("package-1", null);
mockYumVersion();
terminal.expectCommand(
"yum install --assumeyes --setopt skip_missing_names_on_install=False package-1 package-2 package-3 2>&1",
@@ -328,4 +350,7 @@ public class YumTest {
mockYumVersion(YumVersion.rhel7);
}
+ private void mockRpmQuery(String packageName, YumPackageName installedOrNull) {
+ new YumTester(terminal).expectQueryInstalled(packageName).andReturn(installedOrNull);
+ }
}