Skip to content

Commit 9848586

Browse files
committed
extensions: add sync, download functionality
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent 5a8a1e2 commit 9848586

28 files changed

Lines changed: 3087 additions & 807 deletions

File tree

api/src/main/java/org/apache/cloudstack/api/ApiConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,7 @@ public class ApiConstants {
546546

547547
public static final String SOURCE_CIDR_LIST = "sourcecidrlist";
548548
public static final String SOURCE_ZONE_ID = "sourcezoneid";
549+
public static final String SOURCE_MANAGEMENT_SERVER_ID = "sourcemanagementserverid";
549550
public static final String SSL_VERIFICATION = "sslverification";
550551
public static final String START_ASN = "startasn";
551552
public static final String START_DATE = "startdate";
@@ -566,6 +567,7 @@ public class ApiConstants {
566567
public static final String SWAP_OWNER = "swapowner";
567568
public static final String SYSTEM_VM_TYPE = "systemvmtype";
568569
public static final String TAGS = "tags";
570+
public static final String TARGET_MANAGEMENT_SERVER_IDS = "targetmanagementserverids";
569571
public static final String STORAGE_TAGS = "storagetags";
570572
public static final String STORAGE_ACCESS_GROUPS = "storageaccessgroups";
571573
public static final String STORAGE_ACCESS_GROUP = "storageaccessgroup";

client/conf/server.properties.in

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,18 @@ extensions.deployment.mode=@EXTENSIONSDEPLOYMENTMODE@
6262
# Thread pool configuration
6363
#threads.min=10
6464
#threads.max=500
65+
66+
# These properties configure the share endpoint, which enables controlled file sharing through the management server.
67+
# They allow administrators to enable or disable sharing, set the base directory for shared files, define cache
68+
# behavior, restrict access to specific directories, and secure access with a secret key. This ensures flexible and
69+
# secure file sharing for different modules such as extensions, etc.
70+
# Enable or disable file sharing feature (true/false). Default is true
71+
# share.enabled=true
72+
# The base directory from which files can be shared. Default is <HOME_DIRECTORY_OF_CLOUD_USER>/share
73+
# share.base.dir=
74+
# The cache control header value to be used for shared files. Default is public,max-age=86400,immutable
75+
# share.cache.control=public,max-age=86400,immutable
76+
# Allow or disallow directory listing when accessing a directory. Default is false
77+
# share.dir.allowed=false
78+
# Secret key for securing links using HMAC signature. Default is empty (no security)
79+
# share.secret=

client/src/main/java/org/apache/cloudstack/ServerDaemon.java

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,25 @@
2424
import java.io.InputStream;
2525
import java.lang.management.ManagementFactory;
2626
import java.net.URL;
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
29+
import java.nio.file.Paths;
2730
import java.util.Arrays;
31+
import java.util.EnumSet;
32+
import java.util.List;
2833
import java.util.Properties;
2934

30-
import com.cloud.api.ApiServer;
35+
import javax.servlet.DispatcherType;
36+
37+
import org.apache.cloudstack.utils.server.ServerPropertiesUtil;
3138
import org.apache.commons.daemon.Daemon;
3239
import org.apache.commons.daemon.DaemonContext;
3340
import org.apache.commons.lang3.StringUtils;
41+
import org.apache.logging.log4j.LogManager;
42+
import org.apache.logging.log4j.Logger;
3443
import org.eclipse.jetty.jmx.MBeanContainer;
3544
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
45+
import org.eclipse.jetty.server.Handler;
3646
import org.eclipse.jetty.server.HttpConfiguration;
3747
import org.eclipse.jetty.server.HttpConnectionFactory;
3848
import org.eclipse.jetty.server.RequestLog;
@@ -46,14 +56,18 @@
4656
import org.eclipse.jetty.server.handler.RequestLogHandler;
4757
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
4858
import org.eclipse.jetty.server.session.SessionHandler;
59+
import org.eclipse.jetty.servlet.DefaultServlet;
60+
import org.eclipse.jetty.servlet.FilterHolder;
61+
import org.eclipse.jetty.servlet.ServletContextHandler;
62+
import org.eclipse.jetty.servlet.ServletHolder;
63+
import org.eclipse.jetty.util.resource.Resource;
4964
import org.eclipse.jetty.util.ssl.KeyStoreScanner;
5065
import org.eclipse.jetty.util.ssl.SslContextFactory;
5166
import org.eclipse.jetty.util.thread.QueuedThreadPool;
5267
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
5368
import org.eclipse.jetty.webapp.WebAppContext;
54-
import org.apache.logging.log4j.Logger;
55-
import org.apache.logging.log4j.LogManager;
5669

70+
import com.cloud.api.ApiServer;
5771
import com.cloud.utils.Pair;
5872
import com.cloud.utils.PropertiesUtil;
5973
import com.cloud.utils.server.ServerProperties;
@@ -89,6 +103,11 @@ public class ServerDaemon implements Daemon {
89103
private static final String THREADS_MIN = "threads.min";
90104
private static final String THREADS_MAX = "threads.max";
91105

106+
public static final String SHARE_ENABLED = "share.enabled";
107+
public static final String SHARE_CACHE_CONTROL = "share.cache.control";
108+
public static final String SHARE_DIR_ALLOWED = "share.dir.allowed";
109+
public static final String SHARE_SECRET = "share.secret";
110+
92111
////////////////////////////////////////////////////////
93112
/////////////// Server Configuration ///////////////////
94113
////////////////////////////////////////////////////////
@@ -111,6 +130,12 @@ public class ServerDaemon implements Daemon {
111130
private int minThreads;
112131
private int maxThreads;
113132

133+
private boolean shareEnabled = false;
134+
private String shareBaseDir;
135+
private String shareCacheCtl;
136+
private boolean shareDirList = false;
137+
private String shareSecret;
138+
114139
//////////////////////////////////////////////////
115140
/////////////// Public methods ///////////////////
116141
//////////////////////////////////////////////////
@@ -121,6 +146,14 @@ public static void main(final String... anArgs) throws Exception {
121146
daemon.start();
122147
}
123148

149+
protected void initShareConfigFromProperties(Properties properties) {
150+
setShareEnabled(Boolean.parseBoolean(properties.getProperty(SHARE_ENABLED, "true")));
151+
setShareBaseDir(ServerPropertiesUtil.getShareBaseDirectory());
152+
setShareCacheCtl(properties.getProperty(SHARE_CACHE_CONTROL, "public,max-age=86400,immutable"));
153+
setShareDirList(Boolean.parseBoolean(properties.getProperty(SHARE_DIR_ALLOWED, "false")));
154+
setShareSecret(properties.getProperty(SHARE_SECRET, ""));
155+
}
156+
124157
@Override
125158
public void init(final DaemonContext context) {
126159
final File confFile = PropertiesUtil.findConfigFile("server.properties");
@@ -153,6 +186,7 @@ public void init(final DaemonContext context) {
153186
setMaxFormKeys(Integer.valueOf(properties.getProperty(REQUEST_MAX_FORM_KEYS_KEY, String.valueOf(DEFAULT_REQUEST_MAX_FORM_KEYS))));
154187
setMinThreads(Integer.valueOf(properties.getProperty(THREADS_MIN, "10")));
155188
setMaxThreads(Integer.valueOf(properties.getProperty(THREADS_MAX, "500")));
189+
initShareConfigFromProperties(properties);
156190
} catch (final IOException e) {
157191
logger.warn("Failed to read configuration from server.properties file", e);
158192
} finally {
@@ -288,6 +322,52 @@ private void createHttpsConnector(final HttpConfiguration httpConfig) {
288322
}
289323
}
290324

325+
/**
326+
* Creates a Jetty context at /share to serve static files for modules (e.g. Extensions Framework).
327+
* Controlled via server properties
328+
*
329+
* @return a configured Handler or null if disabled.
330+
*/
331+
private Handler createShareContextHandler() throws IOException {
332+
if (!shareEnabled) {
333+
logger.info("{}=false; /{} context not mounted", SHARE_ENABLED, ServerPropertiesUtil.SHARE_DIR);
334+
return null;
335+
}
336+
337+
final Path base = Paths.get(shareBaseDir);
338+
Files.createDirectories(base);
339+
340+
final ServletContextHandler shareCtx = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
341+
shareCtx.setContextPath("/" + ServerPropertiesUtil.SHARE_DIR);
342+
shareCtx.setBaseResource(Resource.newResource(base.toAbsolutePath().toUri()));
343+
344+
// Efficient static file serving
345+
ServletHolder def = shareCtx.addServlet(DefaultServlet.class, "/*");
346+
def.setInitParameter("dirAllowed", Boolean.toString(shareDirList));
347+
def.setInitParameter("etags", "true");
348+
def.setInitParameter("cacheControl", shareCacheCtl);
349+
def.setInitParameter("useFileMappedBuffer", "true");
350+
def.setInitParameter("acceptRanges", "true");
351+
352+
// Gzip using modern Jetty handler
353+
org.eclipse.jetty.server.handler.gzip.GzipHandler gzipHandler =
354+
new org.eclipse.jetty.server.handler.gzip.GzipHandler();
355+
gzipHandler.setMinGzipSize(1024);
356+
gzipHandler.setIncludedMimeTypes(
357+
"text/html", "text/plain", "text/css", "text/javascript",
358+
"application/javascript", "application/json", "application/xml");
359+
gzipHandler.setHandler(shareCtx);
360+
361+
// Optional signed-URL guard (path + "|" + exp => HMAC-SHA256, base64url)
362+
if (StringUtils.isNotBlank(shareSecret)) {
363+
shareCtx.addFilter(new FilterHolder(new ShareSignedUrlFilter(true, shareSecret)),
364+
"/*", EnumSet.of(DispatcherType.REQUEST));
365+
}
366+
367+
logger.info("Mounted /{} static context at baseDir={}", ServerPropertiesUtil.SHARE_DIR, base);
368+
return shareCtx;
369+
}
370+
291371
private Pair<SessionHandler,HandlerCollection> createHandlers() {
292372
final WebAppContext webApp = new WebAppContext();
293373
webApp.setContextPath(contextPath);
@@ -318,8 +398,23 @@ private Pair<SessionHandler,HandlerCollection> createHandlers() {
318398
rootRedirect.setNewContextURL(contextPath);
319399
rootRedirect.setPermanent(true);
320400

401+
// Optional /share handler (served by createShareContextHandler)
402+
Handler shareHandler = null;
403+
try {
404+
shareHandler = createShareContextHandler();
405+
} catch (IOException e) {
406+
logger.error("Failed to initialize /share context", e);
407+
}
408+
409+
List<Handler> handlers = new java.util.ArrayList<>();
410+
handlers.add(log);
411+
handlers.add(gzipHandler);
412+
if (shareHandler != null) {
413+
handlers.add(shareHandler);
414+
}
321415
// Put rootRedirect at the end!
322-
return new Pair<>(webApp.getSessionHandler(), new HandlerCollection(log, gzipHandler, rootRedirect));
416+
handlers.add(rootRedirect);
417+
return new Pair<>(webApp.getSessionHandler(), new HandlerCollection(handlers.toArray(new Handler[0])));
323418
}
324419

325420
private RequestLog createRequestLog() {
@@ -408,4 +503,24 @@ public void setMinThreads(int minThreads) {
408503
public void setMaxThreads(int maxThreads) {
409504
this.maxThreads = maxThreads;
410505
}
506+
507+
public void setShareEnabled(boolean shareEnabled) {
508+
this.shareEnabled = shareEnabled;
509+
}
510+
511+
public void setShareBaseDir(String shareBaseDir) {
512+
this.shareBaseDir = shareBaseDir;
513+
}
514+
515+
public void setShareCacheCtl(String shareCacheCtl) {
516+
this.shareCacheCtl = shareCacheCtl;
517+
}
518+
519+
public void setShareDirList(boolean shareDirList) {
520+
this.shareDirList = shareDirList;
521+
}
522+
523+
public void setShareSecret(String shareSecret) {
524+
this.shareSecret = shareSecret;
525+
}
411526
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack;
19+
20+
import java.io.IOException;
21+
import java.security.InvalidKeyException;
22+
import java.security.NoSuchAlgorithmException;
23+
import java.time.Instant;
24+
25+
import javax.servlet.Filter;
26+
import javax.servlet.FilterChain;
27+
import javax.servlet.ServletException;
28+
import javax.servlet.ServletRequest;
29+
import javax.servlet.ServletResponse;
30+
import javax.servlet.http.HttpServletRequest;
31+
import javax.servlet.http.HttpServletResponse;
32+
33+
import org.apache.cloudstack.utils.security.HMACSignUtil;
34+
import org.apache.commons.codec.DecoderException;
35+
36+
/**
37+
* Optional HMAC token check: /share/...?...&exp=1699999999&sig=BASE64URL(HMACSHA256(path|exp))
38+
* If no token params are present, request is allowed (set requireToken=true to enforce).
39+
*/
40+
public class ShareSignedUrlFilter implements Filter {
41+
private final boolean requireToken;
42+
private final String secret;
43+
44+
public ShareSignedUrlFilter(boolean requireToken, String secret) {
45+
this.requireToken = requireToken;
46+
this.secret = secret;
47+
}
48+
49+
@Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
50+
throws IOException, ServletException {
51+
HttpServletRequest r = (HttpServletRequest) req;
52+
HttpServletResponse w = (HttpServletResponse) res;
53+
54+
String expStr = r.getParameter("exp");
55+
String sig = r.getParameter("sig");
56+
57+
if (!requireToken && (expStr == null || sig == null)) {
58+
chain.doFilter(req, res);
59+
return;
60+
}
61+
if (expStr == null || sig == null) {
62+
w.sendError(HttpServletResponse.SC_FORBIDDEN, "Missing token");
63+
return;
64+
}
65+
long exp;
66+
try {
67+
exp = Long.parseLong(expStr);
68+
} catch (NumberFormatException e) {
69+
w.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad exp");
70+
return;
71+
}
72+
if (Instant.now().getEpochSecond() > exp) {
73+
w.sendError(HttpServletResponse.SC_FORBIDDEN, "Token expired");
74+
return;
75+
}
76+
String want = "";
77+
try {
78+
String data = r.getRequestURI() + "|" + expStr;
79+
want = HMACSignUtil.generateSignature(data, secret);
80+
} catch (InvalidKeyException | NoSuchAlgorithmException | DecoderException e) {
81+
w.sendError(HttpServletResponse.SC_FORBIDDEN, "Auth error");
82+
return;
83+
}
84+
if (!want.equals(sig)) {
85+
w.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad signature");
86+
return;
87+
}
88+
chain.doFilter(req, res);
89+
}
90+
}
91+

engine/components-api/src/main/java/com/cloud/hypervisor/ExternalProvisioner.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,6 @@
3535

3636
public interface ExternalProvisioner extends Manager {
3737

38-
String getExtensionsPath();
39-
40-
String getExtensionPath(String relativePath);
41-
42-
String getChecksumForExtensionPath(String extensionName, String relativePath);
43-
44-
void prepareExtensionPath(String extensionName, boolean userDefined, String extensionRelativePath);
45-
46-
void cleanupExtensionPath(String extensionName, String extensionRelativePath);
47-
48-
void cleanupExtensionData(String extensionName, int olderThanDays, boolean cleanupDirectory);
49-
5038
PrepareExternalProvisioningAnswer prepareExternalProvisioning(String hostGuid, String extensionName, String extensionRelativePath, PrepareExternalProvisioningCommand cmd);
5139

5240
StartAnswer startInstance(String hostGuid, String extensionName, String extensionRelativePath, StartCommand cmd);

framework/cluster/src/main/java/com/cloud/cluster/ClusterManagerImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,8 @@ public String execute(final String strPeer, final long agentId, final String cmd
468468
return null;
469469
}
470470

471+
472+
471473
@Override
472474
public ManagementServerHostVO getPeer(final String mgmtServerId) {
473475
return _mshostDao.findByMsid(Long.parseLong(mgmtServerId));

0 commit comments

Comments
 (0)