diff options
author | Martin Polden <mpolden@mpolden.no> | 2021-01-14 13:31:31 +0100 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2021-01-14 14:36:31 +0100 |
commit | 4786e790d95bf80b96d8442ef4949235cd5b7f2a (patch) | |
tree | a54401702a6ed44a45a5f98d30261d2d2edf1673 /node-admin | |
parent | 1f9701dfedda7283e151fed7498dcd45d3be0ea6 (diff) |
Handle Yum 4 package name format
Diffstat (limited to 'node-admin')
8 files changed, 165 insertions, 29 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 ecde05af57a..7228741b779 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 @@ -20,6 +20,7 @@ import static com.yahoo.vespa.hosted.node.admin.task.util.yum.YumCommand.Install * @author hakonhall */ public class Yum { + // Note: "(?dm)" makes newline be \n (only), and enables multiline mode where ^$ match lines with find() private static final Pattern INSTALL_NOOP_PATTERN = Pattern.compile("(?dm)^Nothing to do\\.?$"); private static final Pattern UPGRADE_NOOP_PATTERN = Pattern.compile("(?dm)^No packages marked for update$"); @@ -110,4 +111,5 @@ public class Yum { } return array; } + } 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 0cc0e256c54..3ff960d88e2 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 @@ -1,6 +1,7 @@ // Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. 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.Terminal; @@ -18,6 +19,11 @@ import java.util.stream.Collectors; public abstract class YumCommand<T extends YumCommand<T>> { private List<String> enabledRepos = List.of(); + private final Terminal terminal; + + protected YumCommand(Terminal terminal) { + this.terminal = terminal; + } /** Enables the given repos for this command */ public T enableRepo(String... repo) { @@ -34,6 +40,15 @@ public abstract class YumCommand<T extends YumCommand<T>> { public abstract boolean converge(TaskContext context); + /** Returns the version of Yum itself */ + protected final Version version(TaskContext context) { + return terminal.newCommandLine(context).add("yum", "--version") + .executeSilently() + .getOutputLinesStream() + .findFirst() + .map(Version::fromString).orElseThrow(() -> new IllegalStateException("Failed to detect Yum version")); + } + public static class GenericYumCommand extends YumCommand<GenericYumCommand> { private static final Pattern UNKNOWN_PACKAGE_PATTERN = Pattern.compile("(?dm)^No package ([^ ]+) available\\.$"); @@ -45,6 +60,7 @@ public abstract class YumCommand<T extends YumCommand<T>> { private final List<String> options = new ArrayList<>(); GenericYumCommand(Terminal terminal, String yumCommand, List<YumPackageName> packages, Pattern... outputNoopPatterns) { + super(terminal); this.terminal = terminal; this.yumCommand = yumCommand; this.packages = packages; @@ -80,10 +96,11 @@ public abstract class YumCommand<T extends YumCommand<T>> { throw new IllegalArgumentException("No packages specified"); } + Version yumVersion = version(context); CommandLine commandLine = terminal.newCommandLine(context); commandLine.add("yum", yumCommand); addParametersToCommandLine(commandLine); - commandLine.add(packages.stream().map(YumPackageName::toName).collect(Collectors.toList())); + commandLine.add(packages.stream().map(pkg -> pkg.toName(yumVersion)).collect(Collectors.toList())); // There's no way to figure out whether a yum command would have been a no-op. // Therefore, run the command and parse the output to decide. @@ -121,13 +138,15 @@ public abstract class YumCommand<T extends YumCommand<T>> { private final YumPackageName yumPackage; InstallFixedYumCommand(Terminal terminal, YumPackageName yumPackage) { + super(terminal); this.terminal = terminal; this.yumPackage = yumPackage; } @Override public boolean converge(TaskContext context) { - String targetVersionLockName = yumPackage.toVersionLockName(); + Version yumVersion = version(context); + String targetVersionLockName = yumPackage.toVersionLockName(yumVersion); boolean alreadyLocked = terminal .newCommandLine(context) @@ -142,7 +161,7 @@ public abstract class YumCommand<T extends YumCommand<T>> { if (packageName.getName().equals(yumPackage.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 versionLockName = packageName.toVersionLockName(); + String versionLockName = packageName.toVersionLockName(yumVersion); if (versionLockName.equals(targetVersionLockName)) { return true; } else { @@ -180,7 +199,7 @@ public abstract class YumCommand<T extends YumCommand<T>> { var installCommand = terminal.newCommandLine(context).add("yum", "install"); addParametersToCommandLine(installCommand); - installCommand.add(yumPackage.toName()); + installCommand.add(yumPackage.toName(yumVersion)); String output = installCommand.executeSilently().getUntrimmedOutput(); @@ -189,7 +208,7 @@ public abstract class YumCommand<T extends YumCommand<T>> { // case 3. var upgradeCommand = terminal.newCommandLine(context).add("yum", "downgrade"); addParametersToCommandLine(upgradeCommand); - upgradeCommand.add(yumPackage.toName()).execute(); + upgradeCommand.add(yumPackage.toName(yumVersion)).execute(); modified = true; } else { // case 2. @@ -205,4 +224,5 @@ public abstract class YumCommand<T extends YumCommand<T>> { protected InstallFixedYumCommand getThis() { return this; } } + } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageName.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageName.java index 5718c0cb6f3..aa9552f8620 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageName.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageName.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.node.admin.task.util.yum; import com.google.common.base.Strings; +import com.yahoo.component.Version; import java.util.Arrays; import java.util.Objects; @@ -34,6 +35,7 @@ import java.util.stream.Collectors; * @author hakonhall */ public class YumPackageName { + private enum Architecture { noarch, x86_64, i386, i586, i686 } private static final String ARCHITECTURES_OR = @@ -77,8 +79,8 @@ public class YumPackageName { * * <p>WARNING: Should only be invoked if the YUM package actually has an epoch. Typically * YUM packages doesn't have one explicitly set, and in case "0" will be used with - * {@link #toVersionLockName()} (otherwise it fails), but it will be absent from an - * install with {@link #toName()} (otherwise it fails). This typically means that + * {@link #toVersionLockName(Version)} (otherwise it fails), but it will be absent from an + * install with {@link #toName(Version)} (otherwise it fails). This typically means that * you should set this only if the epoch is != "0".</p> */ public Builder setEpoch(String epoch) { this.epoch = Optional.of(epoch); return this; } @@ -227,12 +229,25 @@ public class YumPackageName { public Optional<String> getArchitecture() { return architecture; } /** Return package name, omitting components that are not specified. */ - public String toName() { + public String toName(Version yumVersion) { StringBuilder builder = new StringBuilder(); - epoch.ifPresent(ep -> builder.append(ep).append(':')); - builder.append(name); - version.ifPresent(ver -> builder.append('-').append(ver)); - release.ifPresent(rel -> builder.append('-').append(rel)); + char delimiter; + if (yumVersion.getMajor() < 4) { + epoch.ifPresent(ep -> builder.append(ep).append(':')); + builder.append(name); + delimiter = '-'; + } else { + builder.append(name); + epoch.ifPresent(ep -> builder.append('-').append(ep)); + delimiter = ':'; + } + if (version.isPresent()) { + builder.append(delimiter).append(version.get()); + delimiter = '-'; + } + if (release.isPresent()) { + builder.append(delimiter).append(release.get()); + } architecture.ifPresent(arch -> builder.append('.').append(arch)); return builder.toString(); } @@ -242,13 +257,15 @@ public class YumPackageName { * * @throws IllegalStateException if any field required for the version lock spec is missing */ - public String toVersionLockName() { - 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)), - "*"); + public String toVersionLockName(Version yumVersion) { + Builder b = new Builder(this).setArchitecture("*"); + if (epoch.isEmpty()) { + b.setEpoch("0"); + } + YumPackageName lockSpec = b.build(); + if (lockSpec.getVersion().isEmpty()) throw new IllegalStateException("Version is missing for YUM package " + name); + if (lockSpec.getRelease().isEmpty()) throw new IllegalStateException("Release is missing for YUM package " + name); + return lockSpec.toName(yumVersion); } public boolean isSubsetOf(YumPackageName other) { 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 ebba76e44e4..270f2107b87 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 @@ -1,6 +1,7 @@ // Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.task.util.yum; +import com.yahoo.component.Version; import com.yahoo.vespa.hosted.node.admin.task.util.process.TestChildProcess2; import com.yahoo.vespa.hosted.node.admin.task.util.process.TestTerminal; @@ -16,10 +17,20 @@ import java.util.stream.Stream; public class YumTester extends Yum { private final TestTerminal terminal; + private final Version yumVersion; public YumTester(TestTerminal terminal) { + this(terminal, YumVersion.rhel7); + } + + public YumTester(TestTerminal terminal, YumVersion yumVersion) { super(terminal); this.terminal = terminal; + this.yumVersion = yumVersion.asVersion(); + } + + public Version yumVersion() { + return yumVersion; } public GenericYumCommandExpectation expectInstall(String... packages) { @@ -69,6 +80,10 @@ public class YumTester extends Yum { } } + protected void expectYumVersion() { + terminal.expectCommand("yum --version 2>&1", 0, yumVersion.toFullString() + "\ntrailing garbage\n"); + } + private YumTester execute(String output) { StringBuilder cmd = new StringBuilder(); cmd.append("yum ").append(command).append(" --assumeyes"); @@ -78,7 +93,7 @@ public class YumTester extends Yum { if ("upgrade".equals(command) && packages.size() > 1) cmd.append(" --setopt skip_missing_names_on_update=False"); packages.forEach(pkg -> { - String name = pkg.toName(); + String name = pkg.toName(yumVersion); if (name.contains("(") || name.contains(")")) { // Ugly hack to handle implicit quoting done in com.yahoo.vespa.hosted.node.admin.task.util.process.CommandLine name = "\"" + name + "\""; } @@ -86,6 +101,7 @@ public class YumTester extends Yum { }); cmd.append(" 2>&1"); + expectYumVersion(); terminal.expectCommand(cmd.toString(), 0, output); return YumTester.this; } @@ -97,11 +113,16 @@ public class YumTester extends Yum { } @Override + protected void expectYumVersion() {} + + @Override public YumTester andReturn(boolean value) { // Pretend package is already correctly version locked to simplify expectations - terminal.expectCommand("yum --quiet versionlock list 2>&1", 0, packages.get(0).toVersionLockName()); + terminal.expectCommand("yum --version 2>&1", 0, yumVersion.toFullString() + "\ntrailing garbage\n"); + terminal.expectCommand("yum --quiet versionlock list 2>&1", 0, packages.get(0).toVersionLockName(yumVersion)); return super.andReturn(value); } + } public class QueryInstalledExpectation { diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumVersion.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumVersion.java new file mode 100644 index 00000000000..b0c2805d620 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumVersion.java @@ -0,0 +1,26 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.task.util.yum; + +import com.yahoo.component.Version; + +/** + * Red Hat versions and their associated Yum major version. + * + * @author mpolden + */ +public enum YumVersion { + + rhel7(3), + rhel8(4); + + private final Version version; + + YumVersion(int yumMajor) { + this.version = new Version(yumMajor, 0, 0); + } + + public Version asVersion() { + return version; + } + +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageNameTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageNameTest.java index bc90602dfa3..3a8868d70f9 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageNameTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageNameTest.java @@ -1,6 +1,7 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.task.util.yum; +import com.yahoo.component.Version; import org.junit.Test; import java.util.Optional; @@ -12,7 +13,11 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +/** + * @author hakonhall + */ public class YumPackageNameTest { + @Test public void testBuilder() { YumPackageName yumPackage = new YumPackageName.Builder("docker") @@ -21,7 +26,8 @@ public class YumPackageNameTest { .setRelease("71.git3e8e77d.el7.centos.1") .setArchitecture("x86_64") .build(); - assertEquals("2:docker-1.12.6-71.git3e8e77d.el7.centos.1.x86_64", yumPackage.toName()); + assertEquals("2:docker-1.12.6-71.git3e8e77d.el7.centos.1.x86_64", yumPackage.toName(Version.fromString("3"))); + assertEquals("docker-2:1.12.6-71.git3e8e77d.el7.centos.1.x86_64", yumPackage.toName(Version.fromString("4"))); } @Test @@ -110,23 +116,24 @@ public class YumPackageNameTest { String architecture, String toName, String toVersionName) { + Version yumVersion = Version.fromString("3"); YumPackageName yumPackageName = YumPackageName.fromString(packageName); verifyValue(epoch, yumPackageName.getEpoch()); verifyValue(name, Optional.of(yumPackageName.getName())); verifyValue(version, yumPackageName.getVersion()); verifyValue(release, yumPackageName.getRelease()); verifyValue(architecture, yumPackageName.getArchitecture()); - verifyValue(toName, Optional.of(yumPackageName.toName())); + verifyValue(toName, Optional.of(yumPackageName.toName(yumVersion))); if (toVersionName == null) { try { - yumPackageName.toVersionLockName(); + yumPackageName.toVersionLockName(yumVersion); fail(); } catch (IllegalStateException e) { assertThat(e.getMessage(), containsStringIgnoringCase("Version is missing ")); } } else { - assertEquals(toVersionName, yumPackageName.toVersionLockName()); + assertEquals(toVersionName, yumPackageName.toVersionLockName(yumVersion)); } } @@ -179,4 +186,5 @@ public class YumPackageNameTest { .setVersion("1.13.1") .build())); } + } 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 8c14ceb3c76..a66fc0cceab 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 @@ -16,13 +16,17 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; +/** + * @author hakonhall + */ public class YumTest { + private final TaskContext taskContext = mock(TaskContext.class); private final TestTerminal terminal = new TestTerminal(); private final Yum yum = new Yum(terminal); @After - public void tearDown() { + public void after() { terminal.verifyAllCommandsExecuted(); } @@ -74,6 +78,7 @@ public class YumTest { @Test public void testAlreadyInstalled() { + mockYumVersion(); terminal.expectCommand( "yum install --assumeyes --enablerepo=repo1 --enablerepo=repo2 --setopt skip_missing_names_on_install=False package-1 package-2 2>&1", 0, @@ -85,6 +90,7 @@ public class YumTest { .converge(taskContext)); // RHEL 8 + 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", 0, @@ -96,6 +102,7 @@ public class YumTest { @Test public void testAlreadyUpgraded() { + mockYumVersion(); terminal.expectCommand( "yum upgrade --assumeyes --setopt skip_missing_names_on_update=False package-1 package-2 2>&1", 0, @@ -106,6 +113,7 @@ public class YumTest { .converge(taskContext)); // RHEL 8 + mockYumVersion(YumVersion.rhel8); terminal.expectCommand( "yum upgrade --assumeyes --setopt skip_missing_names_on_update=False package-1 package-2 2>&1", 0, @@ -117,6 +125,7 @@ public class YumTest { @Test public void testAlreadyRemoved() { + mockYumVersion(); terminal.expectCommand( "yum remove --assumeyes package-1 package-2 2>&1", 0, @@ -127,6 +136,7 @@ public class YumTest { .converge(taskContext)); // RHEL 8 + mockYumVersion(YumVersion.rhel8); terminal.expectCommand( "yum remove --assumeyes package-1 package-2 2>&1", 0, @@ -138,6 +148,7 @@ public class YumTest { @Test public void testInstall() { + mockYumVersion(); terminal.expectCommand( "yum install --assumeyes --setopt skip_missing_names_on_install=False package-1 package-2 2>&1", 0, @@ -150,6 +161,7 @@ public class YumTest { @Test public void testInstallWithEnablerepo() { + mockYumVersion(); terminal.expectCommand( "yum install --assumeyes --enablerepo=repo-name --setopt skip_missing_names_on_install=False package-1 package-2 2>&1", 0, @@ -163,6 +175,7 @@ public class YumTest { @Test public void testWithVersionLock() { + mockYumVersion(); terminal.expectCommand("yum --quiet versionlock list 2>&1", 0, "Repository chef_rpms-release is listed more than once in the configuration\n" + @@ -177,7 +190,23 @@ public class YumTest { } @Test + public void testWithVersionLockYum4() { + mockYumVersion(YumVersion.rhel8); + terminal.expectCommand("yum --quiet versionlock list 2>&1", + 0, + "Last metadata expiration check: 0:51:26 ago on Thu 14 Jan 2021 09:39:24 AM UTC.\n"); + terminal.expectCommand("yum versionlock add --assumeyes \"openssh-0:8.0p1-4.el8_1.*\" 2>&1"); + terminal.expectCommand( + "yum install --assumeyes openssh-0:8.0p1-4.el8_1.x86_64 2>&1", + 0, + "installing"); + + assertTrue(yum.installFixedVersion(YumPackageName.fromString("openssh-0:8.0p1-4.el8_1.x86_64")).converge(taskContext)); + } + + @Test public void testWithDifferentVersionLock() { + mockYumVersion(); terminal.expectCommand("yum --quiet versionlock list 2>&1", 0, "Repository chef_rpms-release is listed more than once in the configuration\n" + @@ -202,6 +231,7 @@ public class YumTest { @Test public void testWithExistingVersionLock() { + mockYumVersion(); terminal.expectCommand("yum --quiet versionlock list 2>&1", 0, "Repository chef_rpms-release is listed more than once in the configuration\n" + @@ -217,6 +247,7 @@ public class YumTest { @Test public void testWithDowngrade() { + mockYumVersion(); terminal.expectCommand("yum --quiet versionlock list 2>&1", 0, "Repository chef_rpms-release is listed more than once in the configuration\n" + @@ -236,6 +267,7 @@ public class YumTest { @Test(expected = ChildProcessFailureException.class) public void testFailedInstall() { + mockYumVersion(); terminal.expectCommand( "yum install --assumeyes --enablerepo=repo-name --setopt skip_missing_names_on_install=False package-1 package-2 2>&1", 1, @@ -250,6 +282,7 @@ public class YumTest { @Test public void testUnknownPackages() { + mockYumVersion(); terminal.expectCommand( "yum install --assumeyes --setopt skip_missing_names_on_install=False package-1 package-2 package-3 2>&1", 0, @@ -276,9 +309,17 @@ public class YumTest { @Test public void allowToCallUpgradeWithNoPackages() { + mockYumVersion(); terminal.expectCommand("yum upgrade --assumeyes 2>&1", 0, "OK"); - yum.upgrade().converge(taskContext); } + private void mockYumVersion(YumVersion yumVersion) { + terminal.expectCommand("yum --version 2>&1", 0, yumVersion.asVersion().toFullString() + "\ntrailing garbage\n"); + } + + private void mockYumVersion() { + mockYumVersion(YumVersion.rhel7); + } + } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTesterTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTesterTest.java index ef380046b75..bb4bb2686a3 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTesterTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumTesterTest.java @@ -37,7 +37,7 @@ public class YumTesterTest { assertYumMethod(yum -> yum.expectRemove(packages).withEnableRepo(repos), yum -> yum.remove(List.of(packages)).enableRepo(repos).converge(context)); - assertYumMethod(yum -> yum.expectInstallFixedVersion(minimalPackage.toName()).withEnableRepo(repos), + assertYumMethod(yum -> yum.expectInstallFixedVersion(minimalPackage.toName(yum.yumVersion())).withEnableRepo(repos), yum -> yum.installFixedVersion(minimalPackage).enableRepo(repos).converge(context)); } @@ -58,4 +58,5 @@ public class YumTesterTest { terminal.verifyAllCommandsExecuted(); }); } -}
\ No newline at end of file + +} |