aboutsummaryrefslogtreecommitdiffstats
path: root/node-admin
diff options
context:
space:
mode:
authorHåkon Hallingstad <hakon@oath.com>2018-08-27 17:18:08 +0200
committerHåkon Hallingstad <hakon@oath.com>2018-08-27 17:18:08 +0200
commit1599bbee277b3065ef327bf98acd4cf71a2d6a97 (patch)
treede1ced5cea7b23e1646cf44e2e44a0b56afbae36 /node-admin
parent73ed7ad7c3cbd41d5ca44c4f2f7ae547fe4c5abe (diff)
Add version locking to YUM commands
Diffstat (limited to 'node-admin')
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/Yum.java195
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTest.java63
2 files changed, 253 insertions, 5 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 5d60823d1c5..22fba731b02 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
@@ -6,8 +6,10 @@ import com.yahoo.vespa.hosted.node.admin.task.util.process.CommandLine;
import com.yahoo.vespa.hosted.node.admin.task.util.process.Terminal;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -63,7 +65,9 @@ public class Yum {
private final String yumCommand;
private final List<String> packages;
private final Pattern commandOutputNoopPattern;
+
private Optional<String> enabledRepo = Optional.empty();
+ private boolean lockVersion = false;
private GenericYumCommand(Terminal terminal,
String yumCommand,
@@ -85,8 +89,58 @@ public class Yum {
return this;
}
- public boolean converge(TaskContext taskContext) {
- CommandLine commandLine = terminal.newCommandLine(taskContext);
+ /**
+ * Ensure the version of the installs are locked.
+ *
+ * <p>WARNING: In order to simplify the user interface of {@link #lockVersion()},
+ * the package name specified in the command, e.g. {@link #install(String...)}, MUST be of
+ * a simple format, see {@link PackageName#fromString(String)}.
+ */
+ public GenericYumCommand lockVersion() {
+ packages.forEach(PackageName::fromString); // to throw any parse error here instead of later
+ lockVersion = true;
+ return this;
+ }
+
+ public boolean converge(TaskContext context) {
+ Set<String> packageNamesToLock = new HashSet<>();
+ Set<String> fullPackageNamesToLock = new HashSet<>();
+
+ if (lockVersion) {
+ // Remove all locks for other version
+
+ packages.stream()
+ .map(PackageName::fromString)
+ .map(PackageName.Builder::new)
+ .map(builder -> builder.setArchitecture("*").build())
+ .forEach(packageName -> {
+ packageNamesToLock.add(packageName.getName());
+ fullPackageNamesToLock.add(packageName.toFullName());
+ });
+
+ terminal.newCommandLine(context)
+ .add("yum", "--quiet", "versionlock", "list")
+ .executeSilently()
+ .getOutputLinesStream()
+ .map(PackageName::parseString)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .forEach(packageName -> {
+ // Ignore lines for other packages
+ if (packageNamesToLock.contains(packageName.getName())) {
+ // If existing lock doesn't exactly match the full package name,
+ // it means it's locked to another version and we must remove that lock.
+ String fullName = packageName.toFullName();
+ if (!fullPackageNamesToLock.remove(fullName)) {
+ terminal.newCommandLine(context)
+ .add("yum", "versionlock", "delete", fullName)
+ .execute();
+ }
+ }
+ });
+ }
+
+ CommandLine commandLine = terminal.newCommandLine(context);
commandLine.add("yum", yumCommand, "--assumeyes");
enabledRepo.ifPresent(repo -> commandLine.add("--enablerepo=" + repo));
commandLine.add(packages);
@@ -101,10 +155,16 @@ public class Yum {
commandLine.recordSilentExecutionAsSystemModification();
}
+ fullPackageNamesToLock.forEach(fullPackageName ->
+ terminal.newCommandLine(context)
+ .add("yum", "versionlock", "add", fullPackageName)
+ .execute());
+ modifiedSystem |= !fullPackageNamesToLock.isEmpty();
+
return modifiedSystem;
}
- public boolean mapOutput(String output) {
+ private boolean mapOutput(String output) {
Matcher unknownPackageMatcher = UNKNOWN_PACKAGE_PATTERN.matcher(output);
if (unknownPackageMatcher.find()) {
throw new IllegalArgumentException("Unknown package: " + unknownPackageMatcher.group(1));
@@ -113,4 +173,133 @@ public class Yum {
return !commandOutputNoopPattern.matcher(output).find();
}
}
+
+ /** YUM package name. */
+ private static class PackageName {
+ private static final Pattern ARCHITECTURE_PATTERN = Pattern.compile("\\.(noarch|x86_64|i686|i386|\\*)$");
+ private static final Pattern NAME_VER_REL_PATTERN = Pattern.compile("^(.+)-([^-]+)-([^-]+)$");
+
+ public final Optional<String> epoch;
+ public final String name;
+ public final Optional<String> version;
+ public final Optional<String> release;
+ public final Optional<String> architecture;
+
+ private PackageName(Optional<String> epoch,
+ String name,
+ Optional<String> version,
+ Optional<String> release,
+ Optional<String> architecture) {
+ this.epoch = epoch;
+ this.name = name;
+ this.version = version;
+ this.release = release;
+ this.architecture = architecture;
+ }
+
+ public static PackageName fromComponents(String name, String version, String release) {
+ if (name.isEmpty()) throw new IllegalArgumentException("Name is empty");
+ if (version.isEmpty()) throw new IllegalArgumentException("Version is empty");
+ if (release.isEmpty()) throw new IllegalArgumentException("Release is empty");
+ return new PackageName(Optional.empty(), name, Optional.of(version), Optional.of(release), Optional.empty());
+ }
+
+ /**
+ * Parse the string specification of a YUM package.
+ *
+ * <p>According to yum(8) a package can be specified using a variety of different
+ * and ambiguous formats. We'll use a subset:
+ *
+ * <ul>
+ * <li>spec MUST be of the form name-ver-rel, name-ver-rel.arch, or epoch:name-ver-rel.arch.
+ * <li>If specified, arch MUST be one of "noarch", "i686", "x86_64", or "*". The wildcard
+ * is equivalent to not specifying arch.
+ * <li>rel cannot end in something that would be mistaken for the '.arch' suffix.
+ * <li>ver and rel are assumed to not contain any '-' to uniquely identify name.
+ * </ul>
+ *
+ * @param spec A package name of the form epoch:name-ver-rel.arch, name-ver-rel.arch, or name-ver-rel.
+ * @return The package with that name.
+ * @throws IllegalArgumentException if spec does not specify a package name.
+ */
+ public static PackageName fromString(String spec) {
+ return parseString(spec).orElseThrow(() -> new IllegalArgumentException("Failed to decode the YUM package spec '" + spec + "'"));
+ }
+
+ /** See {@link #fromString(String)}. */
+ public static Optional<PackageName> parseString(String spec) {
+ Optional<String> epoch = Optional.empty();
+ int epochColon = spec.indexOf(':');
+ if (epochColon >= 0) {
+ epoch = Optional.of(spec.substring(0, epochColon));
+ spec = spec.substring(epochColon + 1);
+ }
+
+ Optional<String> architecture = Optional.empty();
+ Matcher architectureMatcher = ARCHITECTURE_PATTERN.matcher(spec);
+ if (architectureMatcher.find()) {
+ architecture = Optional.of(architectureMatcher.group(1));
+ spec = spec.substring(0, architectureMatcher.start());
+ }
+
+
+ Matcher matcher = NAME_VER_REL_PATTERN.matcher(spec);
+ if (matcher.find()) {
+ return Optional.of(new PackageName(
+ epoch,
+ matcher.group(1),
+ Optional.of(matcher.group(2)),
+ Optional.of(matcher.group(3)),
+ architecture));
+ }
+
+ return Optional.empty();
+ }
+
+ public Optional<String> getEpoch() { return epoch; }
+ public String getName() { return name; }
+ public Optional<String> getVersion() { return version; }
+ public Optional<String> getRelease() { return release; }
+ public Optional<String> getArchitecture() { return architecture; }
+
+ /**
+ * Return the full name of the package in the format epoch:name-ver-rel.arch, which can
+ * be used with e.g. the YUM install and versionlock commands.
+ *
+ * <p>The package MUST have both version and release. Absent epoch defaults to "0".
+ * Absent arch defaults to "*".
+ */
+ public String toFullName() {
+ return String.format("%s:%s-%s-%s.%s",
+ epoch.orElse("0"),
+ name,
+ version.orElseThrow(() -> new IllegalStateException("Version is missing for YUM package " + name)),
+ release.orElseThrow(() -> new IllegalStateException("Release is missing for YUM package " + name)),
+ architecture.orElse("*"));
+ }
+
+ public static class Builder {
+ private Optional<String> epoch;
+ private String name;
+ private Optional<String> version;
+ private Optional<String> release;
+ private Optional<String> architecture;
+
+ public Builder(PackageName aPackage) {
+ epoch = aPackage.epoch;
+ name = aPackage.name;
+ version = aPackage.version;
+ release = aPackage.release;
+ architecture = aPackage.architecture;
+ }
+
+ public Builder setEpoch(String epoch) { this.epoch = Optional.of(epoch); return this; }
+ public Builder setName(String name) { this.name = name; return this; }
+ public Builder setRelease(String version) { this.version = Optional.of(version); return this; }
+ public Builder setVersion(String release) { this.release = Optional.of(release); return this; }
+ public Builder setArchitecture(String architecture) { this.architecture = Optional.of(architecture); return this; }
+
+ public PackageName build() { return new PackageName(epoch, name, version, release, architecture); }
+ }
+ }
}
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 7f37336db70..f2a2306263a 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
@@ -4,7 +4,7 @@ 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.ChildProcessFailureException;
import com.yahoo.vespa.hosted.node.admin.task.util.process.TestTerminal;
-import org.junit.Before;
+import org.junit.After;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -19,7 +19,7 @@ public class YumTest {
private final TestTerminal terminal = new TestTerminal();
private final Yum yum = new Yum(terminal);
- @Before
+ @After
public void tearDown() {
terminal.verifyAllCommandsExecuted();
}
@@ -86,6 +86,65 @@ public class YumTest {
.converge(taskContext));
}
+ @Test
+ public void testWithVersionLock() {
+ terminal.expectCommand("yum --quiet versionlock list 2>&1",
+ 0,
+ "Repository chef_rpms-release is listed more than once in the configuration\n" +
+ "0:chef-12.21.1-1.el7.*\n");
+ terminal.expectCommand(
+ "yum install --assumeyes \"0:package-1-0.10-654.el7.*\" 2>&1",
+ 0,
+ "installing");
+ terminal.expectCommand("yum versionlock add \"0:package-1-0.10-654.el7.*\" 2>&1");
+
+ assertTrue(yum
+ .install("0:package-1-0.10-654.el7.*")
+ .lockVersion()
+ .converge(taskContext));
+ }
+
+ @Test
+ public void testWithDifferentVersionLock() {
+ terminal.expectCommand("yum --quiet versionlock list 2>&1",
+ 0,
+ "Repository chef_rpms-release is listed more than once in the configuration\n" +
+ "0:chef-12.21.1-1.el7.*\n" +
+ "0:package-1-0.1-8.el7.*\n");
+
+ terminal.expectCommand("yum versionlock delete \"0:package-1-0.1-8.el7.*\" 2>&1");
+
+ terminal.expectCommand(
+ "yum install --assumeyes \"0:package-1-0.10-654.el7.*\" 2>&1",
+ 0,
+ "Nothing to do\n");
+
+ terminal.expectCommand("yum versionlock add \"0:package-1-0.10-654.el7.*\" 2>&1");
+
+ assertTrue(yum
+ .install("0:package-1-0.10-654.el7.*")
+ .lockVersion()
+ .converge(taskContext));
+ }
+
+ @Test
+ public void testWithExistingVersionLock() {
+ terminal.expectCommand("yum --quiet versionlock list 2>&1",
+ 0,
+ "Repository chef_rpms-release is listed more than once in the configuration\n" +
+ "0:chef-12.21.1-1.el7.*\n" +
+ "0:package-1-0.10-654.el7.*\n");
+ terminal.expectCommand(
+ "yum install --assumeyes \"0:package-1-0.10-654.el7.*\" 2>&1",
+ 0,
+ "Nothing to do\n");
+
+ assertFalse(yum
+ .install("0:package-1-0.10-654.el7.*")
+ .lockVersion()
+ .converge(taskContext));
+ }
+
@Test(expected = ChildProcessFailureException.class)
public void testFailedInstall() {
terminal.expectCommand(