diff --git a/src/main/java/com/aws/greengrass/componentmanager/ComponentManager.java b/src/main/java/com/aws/greengrass/componentmanager/ComponentManager.java index 762401fba3..f850a32c2c 100644 --- a/src/main/java/com/aws/greengrass/componentmanager/ComponentManager.java +++ b/src/main/java/com/aws/greengrass/componentmanager/ComponentManager.java @@ -61,6 +61,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -71,6 +72,7 @@ import static com.aws.greengrass.componentmanager.KernelConfigResolver.PREV_VERSION_CONFIG_KEY; import static com.aws.greengrass.componentmanager.KernelConfigResolver.VERSION_CONFIG_KEY; +import static com.aws.greengrass.deployment.DeviceConfiguration.DEFAULT_NUCLEUS_COMPONENT_NAME; import static com.aws.greengrass.deployment.converter.DeploymentDocumentConverter.ANY_VERSION; import static org.apache.commons.io.FileUtils.ONE_MB; @@ -296,6 +298,37 @@ private void storeRecipeDigestInConfigStoreForPlugin( } } + /** + * Un-archives the artifacts for the current Nucleus version package. + * + * @return list of un-archived paths + * @throws PackageLoadingException when unable to load current Nucleus + */ + public List unArchiveCurrentNucleusVersionArtifacts() throws PackageLoadingException { + String currentNucleusVersion = deviceConfiguration.getNucleusVersion(); + ComponentIdentifier nucleusComponentIdentifier = + new ComponentIdentifier(DEFAULT_NUCLEUS_COMPONENT_NAME, new Semver(currentNucleusVersion)); + List nucleusArtifactFileNames = + componentStore.getArtifactFiles(nucleusComponentIdentifier, artifactDownloaderFactory); + return nucleusArtifactFileNames.stream() + .map(file -> { + try { + Path unarchivePath = + nucleusPaths.unarchiveArtifactPath(nucleusComponentIdentifier, getFileName(file)); + /* + Using a hard-coded ZIP un-archiver as today this code path is only used to un-archive a Nucleus + .zip artifact. + */ + unarchiver.unarchive(Unarchive.ZIP, file, unarchivePath); + return unarchivePath; + } catch (IOException e) { + logger.atDebug().setCause(e).kv("comp-id", nucleusComponentIdentifier) + .log("Could not un-archive Nucleus artifact"); + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + private Optional findBestCandidateLocally(String componentName, Map versionRequirements) throws PackagingException { diff --git a/src/main/java/com/aws/greengrass/componentmanager/ComponentStore.java b/src/main/java/com/aws/greengrass/componentmanager/ComponentStore.java index f4346c221c..963617f0f1 100644 --- a/src/main/java/com/aws/greengrass/componentmanager/ComponentStore.java +++ b/src/main/java/com/aws/greengrass/componentmanager/ComponentStore.java @@ -47,8 +47,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.LongStream; import javax.inject.Inject; @@ -412,6 +414,37 @@ public Path resolveArtifactDirectoryPath(@NonNull ComponentIdentifier componentI } } + /** + * Returns the artifact file name. + * + * @param componentIdentifier packageIdentifier + * @param artifactDownloaderFactory artifact downloader factory + * @return the unarchive artifact directory path for target package. + * @throws PackageLoadingException if creating the directory fails + */ + public List getArtifactFiles(@NonNull ComponentIdentifier componentIdentifier, + @NonNull ArtifactDownloaderFactory artifactDownloaderFactory) + throws PackageLoadingException { + Optional componentRecipeContent = findComponentRecipeContent(componentIdentifier); + if (!componentRecipeContent.isPresent()) { + return Collections.emptyList(); + } + + ComponentRecipe recipe = getPackageRecipe(componentIdentifier); + Path packageArtifactDirectory = resolveArtifactDirectoryPath(componentIdentifier); + return recipe.getArtifacts().stream().map(artifact -> { + try { + return artifactDownloaderFactory + .getArtifactDownloader(componentIdentifier, artifact, packageArtifactDirectory) + .getArtifactFile(); + } catch (PackageLoadingException | InvalidArtifactUriException e) { + logger.atDebug().setCause(e).kv("comp-id", componentRecipeContent) + .log("Could not get artifact file"); + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + /** * Resolve the recipe file path for a target package id. * diff --git a/src/main/java/com/aws/greengrass/deployment/errorcode/DeploymentErrorCode.java b/src/main/java/com/aws/greengrass/deployment/errorcode/DeploymentErrorCode.java index ec604e9b33..4ee20370d4 100644 --- a/src/main/java/com/aws/greengrass/deployment/errorcode/DeploymentErrorCode.java +++ b/src/main/java/com/aws/greengrass/deployment/errorcode/DeploymentErrorCode.java @@ -64,7 +64,7 @@ public enum DeploymentErrorCode { // JVM hashing issue HASHING_ALGORITHM_UNAVAILABLE(DeploymentErrorType.DEVICE_ERROR), // Could be a local file issue or a Nucleus issue; we will categorize as the latter for visibility - LAUNCH_DIRECTORY_CORRUPTED(DeploymentErrorType.NUCLEUS_ERROR), + LAUNCH_DIRECTORY_CORRUPTED(DeploymentErrorType.DEVICE_ERROR), /* Component recipe errors */ RECIPE_PARSE_ERROR(DeploymentErrorType.COMPONENT_RECIPE_ERROR), diff --git a/src/main/java/com/aws/greengrass/lifecyclemanager/KernelAlternatives.java b/src/main/java/com/aws/greengrass/lifecyclemanager/KernelAlternatives.java index 3bffaaa754..4e872ba294 100644 --- a/src/main/java/com/aws/greengrass/lifecyclemanager/KernelAlternatives.java +++ b/src/main/java/com/aws/greengrass/lifecyclemanager/KernelAlternatives.java @@ -5,6 +5,8 @@ package com.aws.greengrass.lifecyclemanager; +import com.aws.greengrass.componentmanager.ComponentManager; +import com.aws.greengrass.componentmanager.exceptions.PackageLoadingException; import com.aws.greengrass.config.Configuration; import com.aws.greengrass.config.Topics; import com.aws.greengrass.dependency.Context; @@ -35,6 +37,8 @@ import java.nio.file.Path; import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.Optional; import java.util.Set; import javax.inject.Inject; @@ -70,15 +74,18 @@ public class KernelAlternatives { private static final String BOOTSTRAP_ON_ROLLBACK_CONFIG_KEY = "bootstrapOnRollback"; private final NucleusPaths nucleusPaths; + private final ComponentManager componentManager; /** * Constructor for KernelAlternatives, which manages the alternative launch directory of Kernel. * * @param nucleusPaths nucleus paths + * @param componentManager component manager */ @Inject - public KernelAlternatives(NucleusPaths nucleusPaths) { + public KernelAlternatives(NucleusPaths nucleusPaths, ComponentManager componentManager) { this.nucleusPaths = nucleusPaths; + this.componentManager = componentManager; try { setupInitLaunchDirIfAbsent(); } catch (IOException e) { @@ -162,6 +169,39 @@ public boolean isLaunchDirSetup() { return Files.isSymbolicLink(getCurrentDir()) && validateLaunchDirSetup(getCurrentDir()); } + protected boolean canRecoverMissingLaunchDirSetup() + throws IOException, URISyntaxException, PackageLoadingException { + /* + Try and relink launch dir with the following replacement criteria + 1. check if current Nucleus execution package is valid + 2. un-archive current Nucleus version from component store + 3. fail with DirectoryValidationException if above steps do not satisfy + */ + Path currentNucleusExecutablePath = locateCurrentKernelUnpackDir(); + if (Files.exists(currentNucleusExecutablePath.resolve(KERNEL_BIN_DIR) + .resolve(Platform.getInstance().loaderFilename()))) { + logger.atDebug().kv("path", currentNucleusExecutablePath) + .log("Current Nucleus executable is valid, setting up launch dir"); + relinkInitLaunchDir(currentNucleusExecutablePath, true); + return true; + } + + List localNucleusExecutablePaths = componentManager.unArchiveCurrentNucleusVersionArtifacts(); + if (!localNucleusExecutablePaths.isEmpty()) { + Optional validNucleusExecutablePath = localNucleusExecutablePaths.stream() + .filter(path -> Files.exists(path.resolve(KERNEL_BIN_DIR) + .resolve(Platform.getInstance().loaderFilename()))) + .findFirst(); + if (validNucleusExecutablePath.isPresent()) { + logger.atDebug().kv("path", validNucleusExecutablePath.get()) + .log("Un-archived current Nucleus artifact"); + relinkInitLaunchDir(validNucleusExecutablePath.get(), true); + return true; + } + } + throw new PackageLoadingException("Could not find a valid Nucleus package to recover launch dir setup"); + } + /** * Validate that launch directory is set up. * @@ -169,24 +209,29 @@ public boolean isLaunchDirSetup() { * @throws DeploymentException when user is not allowed to change file permission */ public void validateLaunchDirSetupVerbose() throws DirectoryValidationException, DeploymentException { - Path currentDir = getCurrentDir(); - if (!Files.isSymbolicLink(currentDir)) { - throw new DirectoryValidationException("Missing symlink to current nucleus launch directory"); + try { + if (!Files.isSymbolicLink(getCurrentDir()) || !Files.exists(getLoaderPathFromLaunchDir(getCurrentDir()))) { + logger.atInfo().log("Current launch dir setup is missing, attempting to recover"); + canRecoverMissingLaunchDirSetup(); + } + } catch (PackageLoadingException | IOException ex) { + throw new DirectoryValidationException("Unable to relink init launch directory", ex); + } catch (URISyntaxException ex) { + // TODO: Fix usage of root path with spaces on linux + throw new DeploymentException("Could not parse init launch directory path", ex); } + + Path currentDir = getCurrentDir(); Path loaderPath = getLoaderPathFromLaunchDir(currentDir); - if (Files.exists(loaderPath)) { - if (!loaderPath.toFile().canExecute()) { - // Ensure that the loader is executable so that we can exec it when restarting Nucleus - try { - Platform.getInstance().setPermissions(OWNER_RWX_EVERYONE_RX, loaderPath); - } catch (IOException e) { - throw new DeploymentException( - String.format("Unable to set loader script at %s as executable", loaderPath), e) - .withErrorContext(e, DeploymentErrorCode.SET_PERMISSION_ERROR); - } + if (!loaderPath.toFile().canExecute()) { + // Ensure that the loader is executable so that we can exec it when restarting Nucleus + try { + Platform.getInstance().setPermissions(OWNER_RWX_EVERYONE_RX, loaderPath); + } catch (IOException e) { + throw new DeploymentException( + String.format("Unable to set loader script at %s as executable", loaderPath), e) + .withErrorContext(e, DeploymentErrorCode.SET_PERMISSION_ERROR); } - } else { - throw new DirectoryValidationException("Missing loader file at " + currentDir.toAbsolutePath()); } } diff --git a/src/main/java/com/aws/greengrass/lifecyclemanager/exceptions/DirectoryValidationException.java b/src/main/java/com/aws/greengrass/lifecyclemanager/exceptions/DirectoryValidationException.java index b4efa72735..8450719474 100644 --- a/src/main/java/com/aws/greengrass/lifecyclemanager/exceptions/DirectoryValidationException.java +++ b/src/main/java/com/aws/greengrass/lifecyclemanager/exceptions/DirectoryValidationException.java @@ -15,4 +15,9 @@ public DirectoryValidationException(String message) { super(message); super.addErrorCode(DeploymentErrorCode.LAUNCH_DIRECTORY_CORRUPTED); } + + public DirectoryValidationException(String message, Throwable throwable) { + super(message, throwable); + super.addErrorCode(DeploymentErrorCode.LAUNCH_DIRECTORY_CORRUPTED); + } } \ No newline at end of file diff --git a/src/test/java/com/aws/greengrass/deployment/activator/KernelUpdateActivatorTest.java b/src/test/java/com/aws/greengrass/deployment/activator/KernelUpdateActivatorTest.java index 3922d5e79f..e62fb994f3 100644 --- a/src/test/java/com/aws/greengrass/deployment/activator/KernelUpdateActivatorTest.java +++ b/src/test/java/com/aws/greengrass/deployment/activator/KernelUpdateActivatorTest.java @@ -227,7 +227,7 @@ void GIVEN_launch_dir_corrupted_WHEN_deployment_activate_THEN_deployment_fail(Ex assertEquals(mockException, result.getFailureCause().getCause()); List expectedStack = Arrays.asList("DEPLOYMENT_FAILURE", "LAUNCH_DIRECTORY_CORRUPTED"); - List expectedTypes = Collections.singletonList("NUCLEUS_ERROR"); + List expectedTypes = Collections.singletonList("DEVICE_ERROR"); TestUtils.validateGenerateErrorReport(result.getFailureCause(), expectedStack, expectedTypes); } diff --git a/src/test/java/com/aws/greengrass/lifecyclemanager/KernelAlternativesTest.java b/src/test/java/com/aws/greengrass/lifecyclemanager/KernelAlternativesTest.java index 2e7a67967a..a9fab1c26a 100644 --- a/src/test/java/com/aws/greengrass/lifecyclemanager/KernelAlternativesTest.java +++ b/src/test/java/com/aws/greengrass/lifecyclemanager/KernelAlternativesTest.java @@ -5,9 +5,11 @@ package com.aws.greengrass.lifecyclemanager; +import com.aws.greengrass.componentmanager.ComponentManager; import com.aws.greengrass.config.PlatformResolver; import com.aws.greengrass.deployment.DeploymentDirectoryManager; import com.aws.greengrass.deployment.bootstrap.BootstrapManager; +import com.aws.greengrass.lifecyclemanager.exceptions.DirectoryValidationException; import com.aws.greengrass.testcommons.testutilities.GGExtension; import com.aws.greengrass.util.NucleusPaths; import com.aws.greengrass.util.Utils; @@ -34,9 +36,12 @@ import static org.hamcrest.io.FileMatchers.anExistingFileOrDirectory; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.internal.verification.VerificationModeFactory.times; @@ -46,8 +51,7 @@ class KernelAlternativesTest { @TempDir Path altsDir; @Mock - NucleusPaths nucleusPaths; - + ComponentManager componentManager; private KernelAlternatives kernelAlternatives; @Mock BootstrapManager bootstrapManager; @@ -58,7 +62,7 @@ class KernelAlternativesTest { void beforeEach() throws IOException { NucleusPaths paths = new NucleusPaths("mock_loader_logs.log"); paths.setKernelAltsPath(altsDir); - kernelAlternatives = spy(new KernelAlternatives(paths)); + kernelAlternatives = spy(new KernelAlternatives(paths, componentManager)); } @Test @@ -212,6 +216,38 @@ void GIVEN_launch_params_THEN_write_to_file() throws Exception { assertEquals("mock string", new String(Files.readAllBytes(expectedLaunchParamsPath))); } + @Test + void GIVEN_validate_launch_dir_setup_WHEN_current_link_missing_and_exception_THEN_directory_validation_exception() throws IOException { + // GIVEN + Path outsidePath = createRandomDirectory(); + Path unpackPath = createRandomDirectory(); + Files.createDirectories(unpackPath.resolve("bin")); + String loaderName = "loader"; + if (PlatformResolver.isWindows) { + loaderName = "loader.cmd"; + } + Files.createFile(unpackPath.resolve("bin").resolve(loaderName)); + + Path distroPath = kernelAlternatives.getInitDir().resolve(KERNEL_DISTRIBUTION_DIR); + Files.createDirectories(kernelAlternatives.getInitDir()); + // current -> init + kernelAlternatives.setupLinkToDirectory(kernelAlternatives.getCurrentDir(), kernelAlternatives.getInitDir()); + // init/distro -> outsidePath + kernelAlternatives.setupLinkToDirectory(distroPath, outsidePath); + assertEquals(kernelAlternatives.getInitDir(), Files.readSymbolicLink(kernelAlternatives.getCurrentDir())); + assertEquals(outsidePath, Files.readSymbolicLink(distroPath)); + + // WHEN + Files.deleteIfExists(kernelAlternatives.getCurrentDir()); + lenient().doThrow(new IOException("Random test failure")) + .when(kernelAlternatives).relinkInitLaunchDir(any(Path.class), eq(true)); + + // THEN + DirectoryValidationException ex = assertThrows(DirectoryValidationException.class, + () -> kernelAlternatives.validateLaunchDirSetupVerbose()); + assertEquals(ex.getMessage(), "Unable to relink init launch directory"); + } + private Path createRandomDirectory() throws IOException { Path path = altsDir.resolve(Utils.generateRandomString(4)); Utils.createPaths(path);