This is an automated email from the ASF dual-hosted git repository.

DaanHoogland pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.22 by this push:
     new b56bbc22934 SSVM: entity download URL creation: ensure userdata base 
dir exists (#14447)
b56bbc22934 is described below

commit b56bbc22934d8aecc2fe507d0b2e757ae634512b
Author: dahn <[email protected]>
AuthorDate: Tue Nov 10 10:23:08 2026 +0100

    SSVM: entity download URL creation: ensure userdata base dir exists (#14447)
    
    Co-authored-by: waterWang <[email protected]>
---
 .../storage/template/UploadManagerImpl.java        | 32 ++++++++++++++-
 .../storage/template/UploadManagerImplTest.java    | 46 ++++++++++++++++++++++
 2 files changed, 77 insertions(+), 1 deletion(-)

diff --git 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java
 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java
index aacca9926f7..a57dc18c600 100644
--- 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java
+++ 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java
@@ -64,6 +64,7 @@ public class UploadManagerImpl extends ManagerBase implements 
UploadManager {
 
     protected static final String EXTRACT_USERDATA_DIR = "userdata";
     protected static final String BASE_EXTRACT_PATH = 
String.format("/var/www/html/%s/", EXTRACT_USERDATA_DIR);
+    protected static final String APACHE_USER = "www-data";
 
     public class Completion implements UploadCompleteCallback {
         private final String jobId;
@@ -276,13 +277,19 @@ public class UploadManagerImpl extends ManagerBase 
implements UploadManager {
         // Create the directory structure so that its visible under apache 
server root
         String extractDir = BASE_EXTRACT_PATH;
         extractDir = extractDir + cmd.getFilepathInExtractURL() + 
File.separator;
+        String result = ensureBaseExtractDirectory();
+        if (result != null) {
+            String errorString = String.format("Error in creating base extract 
directory %s owned by %s: %s", BASE_EXTRACT_PATH, APACHE_USER, result);
+            logger.error(errorString);
+            return new CreateEntityDownloadURLAnswer(errorString, 
CreateEntityDownloadURLAnswer.RESULT_FAILURE);
+        }
         Script command = new Script("/bin/su", logger);
         command.add("-s");
         command.add("/bin/bash");
         command.add("-c");
         command.add("mkdir -p " + extractDir);
         command.add("www-data");
-        String result = command.execute();
+        result = command.execute();
         if (result != null) {
             String errorString = "Error in creating directory =" + result;
             logger.error(errorString);
@@ -325,6 +332,29 @@ public class UploadManagerImpl extends ManagerBase 
implements UploadManager {
 
     }
 
+    /**
+     * Makes sure the base extract directory exists and is owned by the apache 
user, so that the apache user
+     * can create the per-entity subdirectory inside it. Stock systemvm 
templates do not ship this directory.
+     * @return null on success, the error output otherwise
+     */
+    protected String ensureBaseExtractDirectory() {
+        if (isBaseExtractDirectoryReady(Path.of(BASE_EXTRACT_PATH))) {
+            return null;
+        }
+        Script command = new Script("install", logger);
+        command.add("-d", "-o", APACHE_USER, "-g", APACHE_USER, "-m", "755", 
BASE_EXTRACT_PATH);
+        return command.execute();
+    }
+
+    protected boolean isBaseExtractDirectoryReady(Path baseDir) {
+        try {
+            return Files.isDirectory(baseDir) && 
APACHE_USER.equals(Files.getOwner(baseDir).getName());
+        } catch (IOException e) {
+            logger.debug("Unable to determine owner of {}: {}", baseDir, 
e.getMessage());
+            return false;
+        }
+    }
+
     @Override
     public Answer 
handleDeleteEntityDownloadURLCommand(DeleteEntityDownloadURLCommand cmd) {
 
diff --git 
a/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/template/UploadManagerImplTest.java
 
b/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/template/UploadManagerImplTest.java
index 9038098e235..4e86a7f1f52 100644
--- 
a/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/template/UploadManagerImplTest.java
+++ 
b/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/template/UploadManagerImplTest.java
@@ -16,29 +16,39 @@
 // under the License.
 package org.apache.cloudstack.storage.template;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
 import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 
+import java.nio.file.Files;
 import java.nio.file.Path;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InjectMocks;
+import org.mockito.MockedConstruction;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
+import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
 
 import com.cloud.agent.api.storage.DeleteEntityDownloadURLCommand;
 import com.cloud.utils.FileUtil;
+import com.cloud.utils.script.Script;
 
 @RunWith(MockitoJUnitRunner.class)
 public class UploadManagerImplTest {
 
+    @Spy
     @InjectMocks
     UploadManagerImpl uploadManager;
 
@@ -82,4 +92,40 @@ public class UploadManagerImplTest {
         
uploadManager.deleteEntitySymlinkRootDirectoryIfNeeded(mock(DeleteEntityDownloadURLCommand.class),
 validLinkPath);
         fileUtilMock.verify(() -> FileUtil.deleteRecursively(any(Path.class)), 
times(1));
     }
+
+    @Test
+    public void baseExtractDirectoryIsNotReadyWhenMissing() {
+        
assertFalse(uploadManager.isBaseExtractDirectoryReady(Path.of("/nonexistent/cloudstack/userdata/")));
+    }
+
+    @Test
+    public void baseExtractDirectoryIsNotReadyWhenNotOwnedByApacheUser() 
throws Exception {
+        Path dir = Files.createTempDirectory("userdata");
+        try {
+            assertFalse(uploadManager.isBaseExtractDirectoryReady(dir));
+        } finally {
+            Files.delete(dir);
+        }
+    }
+
+    @Test
+    public void ensureBaseExtractDirectorySkipsInstallWhenReady() {
+        
doReturn(true).when(uploadManager).isBaseExtractDirectoryReady(any(Path.class));
+        try (MockedConstruction<Script> scripts = 
mockConstruction(Script.class)) {
+            assertNull(uploadManager.ensureBaseExtractDirectory());
+            Assert.assertTrue(scripts.constructed().isEmpty());
+        }
+    }
+
+    @Test
+    public void ensureBaseExtractDirectoryInstallsWhenNotReady() {
+        
doReturn(false).when(uploadManager).isBaseExtractDirectoryReady(any(Path.class));
+        try (MockedConstruction<Script> scripts = 
mockConstruction(Script.class,
+                (script, context) -> 
Mockito.when(script.execute()).thenReturn("Permission denied"))) {
+            Assert.assertEquals("Permission denied", 
uploadManager.ensureBaseExtractDirectory());
+            Assert.assertEquals(1, scripts.constructed().size());
+            Mockito.verify(scripts.constructed().get(0)).add("-d", "-o", 
UploadManagerImpl.APACHE_USER, "-g", UploadManagerImpl.APACHE_USER,
+                    "-m", "755", UploadManagerImpl.BASE_EXTRACT_PATH);
+        }
+    }
 }

Reply via email to