This commit is contained in:
Abhishek Kumar 2026-03-09 13:14:41 +00:00 committed by GitHub
commit 2355563ddc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 839 additions and 21 deletions

View File

@ -375,6 +375,7 @@ public class ApiConstants {
public static final String LB_PROVIDER = "lbprovider";
public static final String MAC_ADDRESS = "macaddress";
public static final String MAC_ADDRESSES = "macaddresses";
public static final String MANIFEST_URL = "manifesturl";
public static final String MANUAL_UPGRADE = "manualupgrade";
public static final String MATCH_TYPE = "matchtype";
public static final String MAX = "max";

View File

@ -50,5 +50,10 @@
<version>4.23.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${cs.jackson.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,96 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.extensions.api;
import java.util.EnumSet;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsImportManager;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.user.Account;
@APICommand(name = "importExtension",
description = "Imports an extension",
responseObject = ExtensionResponse.class,
responseHasSensitiveInfo = false,
entityType = {Extension.class},
authorized = {RoleType.Admin},
since = "4.23.0")
public class ImportExtensionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
@Inject
ExtensionsImportManager extensionsImportManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.MANIFEST_URL, type = CommandType.STRING, required = true,
description = "URL of the extension manifest import file")
private String manifestUrl;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getManifestUrl() {
return manifestUrl;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
Extension extension = extensionsImportManager.importExtension(this);
ExtensionResponse response = extensionsManager.createExtensionResponse(extension,
EnumSet.of(ApiConstants.ExtensionDetails.all));
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.Extension;
}
}

View File

@ -0,0 +1,25 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.extensions.manager;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd;
public interface ExtensionsImportManager {
Extension importExtension(ImportExtensionCmd cmd);
}

View File

@ -0,0 +1,126 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.extensions.manager;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import javax.inject.Inject;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd;
import org.apache.cloudstack.framework.extensions.dao.ExtensionDao;
import org.apache.cloudstack.framework.extensions.util.ExtensionConfig;
import org.apache.cloudstack.framework.extensions.util.YamlParser;
import org.apache.cloudstack.framework.extensions.util.ZipExtractor;
import org.apache.cloudstack.framework.extensions.vo.ExtensionVO;
import org.apache.commons.lang3.StringUtils;
import com.cloud.hypervisor.ExternalProvisioner;
import com.cloud.utils.FileUtil;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallbackWithException;
import com.cloud.utils.exception.CloudRuntimeException;
public class ExtensionsImportManagerImpl extends ManagerBase implements ExtensionsImportManager {
@Inject
ExtensionsManager extensionsManager;
@Inject
ExternalProvisioner externalProvisioner;
@Inject
ExtensionDao extensionDao;
protected Extension importExtensionInternal(String manifestUrl, Path tempDir) {
Path manifestPath = tempDir.resolve("manifest.yaml");
HttpUtils.downloadFileWithProgress(manifestUrl, manifestPath.toString(), logger);
if (!Files.exists(manifestPath)) {
throw new CloudRuntimeException("Failed to download extension manifest from URL: " + manifestUrl);
}
final ExtensionConfig extensionConfig = YamlParser.parseYamlFile(manifestPath.toString());
//Parse the manifest and create the extension
final String name = extensionConfig.metadata.name;
final String extensionArchiveURL = extensionConfig.getArchiveUrl();
ExtensionVO extensionByName = extensionDao.findByName(name);
if (extensionByName != null) {
throw new CloudRuntimeException("Extension by name already exists");
}
if (StringUtils.isBlank(extensionArchiveURL)) {
throw new CloudRuntimeException("Unable to retrieve archive URL for extension source during import");
}
Path extensionArchivePath = tempDir.resolve(UUID.randomUUID() + ".zip");
HttpUtils.downloadFileWithProgress(extensionArchiveURL, extensionArchivePath.toString(), logger);
if (!Files.exists(extensionArchivePath)) {
throw new CloudRuntimeException("Failed to download extension archive from URL: " + extensionArchiveURL);
}
final String extensionRootPath = externalProvisioner.getExtensionsPath() + File.separator + name;
try {
ZipExtractor.extractZipContents(extensionArchivePath.toString(), extensionRootPath);
} catch (IOException e) {
throw new CloudRuntimeException("Failed to extract extension archive during import at: " + extensionRootPath, e);
}
return Transaction.execute((TransactionCallbackWithException<Extension, CloudRuntimeException>) status -> {
Extension extension = extensionsManager.createExtension(name, extensionConfig.metadata.description,
extensionConfig.spec.type, extensionConfig.spec.entrypoint.path, Extension.State.Enabled.name(),
false, Collections.emptyMap());
for (ExtensionConfig.CustomAction action : extensionConfig.spec.customActions) {
Map<Integer, Map<String, String>> parameters = action.getParametersAsMap();
extensionsManager.addCustomAction(action.name, action.description, extension.getId(),
action.resourcetype, action.allowedroletypes, action.timeout, true, parameters,
null, null, Collections.emptyMap());
}
return extension;
});
}
@Override
public Extension importExtension(ImportExtensionCmd cmd) {
final String manifestUrl = cmd.getManifestUrl();
final String extensionsRootPath = externalProvisioner.getExtensionsPath();
Path tempDir;
try {
Path extensionsRootDir = Paths.get(extensionsRootPath);
Files.createDirectories(extensionsRootDir);
tempDir = Files.createTempDirectory(extensionsRootDir, "import-ext-");
} catch (IOException e) {
logger.error("Failed to create working directory for import extension, {}", extensionsRootPath, e);
throw new CloudRuntimeException("Failed to create working directory for import extension", e);
}
try {
return importExtensionInternal(manifestUrl, tempDir);
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw e;
} finally {
FileUtil.deletePath(tempDir.toString());
}
}
}

View File

@ -57,6 +57,9 @@ public interface ExtensionsManager extends Manager {
Extension createExtension(CreateExtensionCmd cmd);
Extension createExtension(String name, String description, String type, String relativePath, String state,
Boolean orchestratorRequiresPrepareVm, Map<String, String> details);
boolean prepareExtensionPathAcrossServers(Extension extension);
List<ExtensionResponse> listExtensions(ListExtensionsCmd cmd);
@ -79,6 +82,10 @@ public interface ExtensionsManager extends Manager {
ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd);
ExtensionCustomAction addCustomAction(String name, String description, long extensionId, String resourceTypeStr,
List<String> rolesStrList, int timeout , boolean enabled, Map parametersMap, String successMessage,
String errorMessage, Map<String, String> details);
boolean deleteCustomAction(DeleteCustomActionCmd cmd);
List<ExtensionCustomActionResponse> listCustomActions(ListCustomActionCmd cmd);

View File

@ -68,6 +68,7 @@ import org.apache.cloudstack.framework.extensions.api.AddCustomActionCmd;
import org.apache.cloudstack.framework.extensions.api.CreateExtensionCmd;
import org.apache.cloudstack.framework.extensions.api.DeleteCustomActionCmd;
import org.apache.cloudstack.framework.extensions.api.DeleteExtensionCmd;
import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd;
import org.apache.cloudstack.framework.extensions.api.ListCustomActionCmd;
import org.apache.cloudstack.framework.extensions.api.ListExtensionsCmd;
import org.apache.cloudstack.framework.extensions.api.RegisterExtensionCmd;
@ -570,13 +571,8 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
@Override
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CREATE, eventDescription = "creating extension")
public Extension createExtension(CreateExtensionCmd cmd) {
final String name = cmd.getName();
final String description = cmd.getDescription();
final String typeStr = cmd.getType();
String relativePath = cmd.getPath();
final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm();
final String stateStr = cmd.getState();
public Extension createExtension(String name, String description, String typeStr, String relativePath,
String stateStr, Boolean orchestratorRequiresPrepareVm, Map<String, String> details) {
ExtensionVO extensionByName = extensionDao.findByName(name);
if (extensionByName != null) {
throw new CloudRuntimeException("Extension by name already exists");
@ -612,7 +608,6 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
}
extension = extensionDao.persist(extension);
Map<String, String> details = cmd.getDetails();
List<ExtensionDetailsVO> detailsVOList = new ArrayList<>();
if (MapUtils.isNotEmpty(details)) {
for (Map.Entry<String, String> entry : details.entrySet()) {
@ -640,6 +635,22 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
return extensionVO;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CREATE, eventDescription = "creating extension")
public Extension createExtension(CreateExtensionCmd cmd) {
final String name = cmd.getName();
final String description = cmd.getDescription();
final String typeStr = cmd.getType();
String relativePath = cmd.getPath();
final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm();
final String stateStr = cmd.getState();
final Map<String, String> details = cmd.getDetails();
return createExtension(name, description, typeStr, relativePath, stateStr, orchestratorRequiresPrepareVm,
details);
}
@Override
public boolean prepareExtensionPathAcrossServers(Extension extension) {
boolean prepared = true;
@ -973,18 +984,10 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
@Override
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_ADD, eventDescription = "adding extension custom action")
public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) {
String name = cmd.getName();
String description = cmd.getDescription();
Long extensionId = cmd.getExtensionId();
String resourceTypeStr = cmd.getResourceType();
List<String> rolesStrList = cmd.getAllowedRoleTypes();
final int timeout = ObjectUtils.defaultIfNull(cmd.getTimeout(), 3);
final boolean enabled = cmd.isEnabled();
Map parametersMap = cmd.getParametersMap();
final String successMessage = cmd.getSuccessMessage();
final String errorMessage = cmd.getErrorMessage();
Map<String, String> details = cmd.getDetails();
public ExtensionCustomAction addCustomAction(String name, String description, long extensionId,
String resourceTypeStr, List<String> rolesStrList, int timeout , boolean enabled, Map parametersMap,
String successMessage, String errorMessage, Map<String, String> details) {
if (name == null || !name.matches("^[a-zA-Z0-9 _-]+$")) {
throw new InvalidParameterValueException(String.format("Invalid action name: %s. It can contain " +
"only alphabets, numbers, hyphen, underscore and space", name));
@ -1004,7 +1007,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
if (resourceType == null) {
throw new InvalidParameterValueException(
String.format("Invalid resource type specified: %s. Valid values are: %s", resourceTypeStr,
EnumSet.allOf(ExtensionCustomAction.ResourceType.class)));
EnumSet.allOf(ExtensionCustomAction.ResourceType.class)));
}
}
if (resourceType == null && Extension.Type.Orchestrator.equals(extensionVO.getType())) {
@ -1050,6 +1053,24 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
});
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_ADD, eventDescription = "adding extension custom action")
public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) {
String name = cmd.getName();
String description = cmd.getDescription();
Long extensionId = cmd.getExtensionId();
String resourceTypeStr = cmd.getResourceType();
List<String> rolesStrList = cmd.getAllowedRoleTypes();
final int timeout = ObjectUtils.defaultIfNull(cmd.getTimeout(), 3);
final boolean enabled = cmd.isEnabled();
Map parametersMap = cmd.getParametersMap();
final String successMessage = cmd.getSuccessMessage();
final String errorMessage = cmd.getErrorMessage();
Map<String, String> details = cmd.getDetails();
return addCustomAction(name, description, extensionId, resourceTypeStr, rolesStrList, timeout, enabled,
parametersMap, successMessage, errorMessage, details);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_DELETE, eventDescription = "deleting extension custom action")
public boolean deleteCustomAction(DeleteCustomActionCmd cmd) {
@ -1637,6 +1658,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
cmds.add(RunCustomActionCmd.class);
cmds.add(CreateExtensionCmd.class);
cmds.add(ImportExtensionCmd.class);
cmds.add(ListExtensionsCmd.class);
cmds.add(DeleteExtensionCmd.class);
cmds.add(UpdateExtensionCmd.class);

View File

@ -0,0 +1,139 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.extensions.util;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.ApiConstants;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ExtensionConfig {
public String apiVersion;
public String kind;
public Metadata metadata;
public Source source;
public Spec spec;
String archiveUrl;
// -----------------------------
// Nested compact model
// -----------------------------
public static class Metadata {
public String name;
public String displayName;
public String description;
public String version;
public String maintainer;
public String homepage;
}
public static class Source {
public String type;
public String url;
public String refs;
}
public static class Spec {
public String type;
public Compatibility compatibility;
public Entrypoint entrypoint;
public Orchestrator orchestrator;
private Map<String, String> details;
public boolean enabled;
public List<CustomAction> customActions;
public Map<String, String> getDetails() {
return details;
}
public void setDetails(Map<String, String> details) {
this.details = details;
}
}
public static class Compatibility {
public CloudStack cloudstack;
}
public static class CloudStack {
public String minVersion;
}
public static class Entrypoint {
public String language;
public String path;
public String targetDir;
}
public static class Orchestrator {
public boolean requiresPrepareVm;
}
public static class CustomAction {
public String name;
public String displayName;
public String description;
public String resourcetype;
public boolean enabled;
public int timeout;
public List<String> allowedroletypes;
public List<Parameter> parameters;
public Map<Integer, Map<String, String>> getParametersAsMap() {
Map<Integer, Map<String, String>> paramMap = new HashMap<>();
int index = 0;
for (Parameter param : parameters) {
Map<String, String> singleParamMap = new HashMap<>();
singleParamMap.put(ApiConstants.NAME, param.name);
singleParamMap.put(ApiConstants.TYPE, param.type);
singleParamMap.put(ApiConstants.VALIDATION_FORMAT, param.validationformat);
singleParamMap.put(ApiConstants.REQUIRED, Boolean.toString(param.required));
paramMap.put(index++, singleParamMap);
}
return paramMap;
}
}
public static class Parameter {
public String name;
public String type;
public String validationformat;
public boolean required;
}
public String getArchiveUrl() {
String type = source != null ? source.type : null;
if ("git".equalsIgnoreCase(type) && source.url != null && source.url.contains("github.com")) {
// ToDo: improve
String ref = source.refs != null ? source.refs : "main";
return source.url.replace("github.com", "codeload.github.com") + "/zip/refs/heads/" + ref;
}
return source == null ? null : source.url;
}
public Spec getSpec() {
return spec;
}
}

View File

@ -0,0 +1,40 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.extensions.util;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import com.cloud.utils.exception.CloudRuntimeException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
public class YamlParser {
private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
public static ExtensionConfig parseYamlFile(String filePath) {
ExtensionConfig extensionConfig = null;
try (InputStream in = Files.newInputStream(Path.of(filePath))) {
extensionConfig = mapper.readValue(in, ExtensionConfig.class);
} catch (Exception ex) {
throw new CloudRuntimeException("Failed to parse YAML", ex);
}
return extensionConfig;
}
}

View File

@ -0,0 +1,87 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.extensions.util;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ZipExtractor {
/**
* Extracts a GitHub ZIP file contents directly into destDir, skipping top-level folder.
*
* @param zipFilePath Path to the ZIP file
* @param destDir Destination directory
*/
public static void extractZipContents(String zipFilePath, String destDir) throws IOException {
Path destPath = Paths.get(destDir);
if (!Files.exists(destPath)) {
Files.createDirectories(destPath);
}
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(Paths.get(zipFilePath)))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
String entryName = entry.getName();
// Skip the top-level folder (everything before first '/')
int firstSlash = entryName.indexOf('/');
if (firstSlash >= 0) {
entryName = entryName.substring(firstSlash + 1);
}
if (entryName.isEmpty()) {
zis.closeEntry();
continue; // skip the top-level folder itself
}
Path newPath = safeResolve(destPath, entryName);
if (entry.isDirectory()) {
Files.createDirectories(newPath);
} else {
if (newPath.getParent() != null) {
Files.createDirectories(newPath.getParent());
}
try (OutputStream os = Files.newOutputStream(newPath)) {
zis.transferTo(os);
}
}
zis.closeEntry();
}
}
}
/**
* Protects from ZIP Slip vulnerability.
*/
private static Path safeResolve(Path destDir, String entryName) throws IOException {
Path resolved = destDir.resolve(entryName).normalize();
if (!resolved.startsWith(destDir)) {
throw new IOException("ZIP entry outside target dir: " + entryName);
}
return resolved;
}
}

View File

@ -31,6 +31,7 @@
<bean id="ExtensionResourceMapDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDaoImpl" />
<bean id="ExtensionResourceMapDetailsDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDetailsDaoImpl" />
<bean id="ExtensionsManager" class="org.apache.cloudstack.framework.extensions.manager.ExtensionsManagerImpl" />
<bean id="ExtensionsImportManager" class="org.apache.cloudstack.framework.extensions.manager.ExtensionsImportManagerImpl" />
<bean id="ExtensionCustomActionDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDaoImpl" />
<bean id="ExtensionCustomActionDetailsDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDetailsDaoImpl" />
</beans>

View File

@ -0,0 +1,32 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.extensions.util;
import org.junit.Test;
import junit.framework.TestCase;
public class YamlParserTest extends TestCase {
@Test
public void testParseYaml() {
String yamlFilePath = getClass().getResource("testmanifest.yaml").getFile();
ExtensionConfig config = YamlParser.parseYamlFile(yamlFilePath);
assertNotNull(config);
}
}

View File

@ -0,0 +1,110 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
apiVersion: cloudstack.apache.org/v1
kind: OrchestratorExtension
metadata:
name: test
displayName: Test Extension
description: >
Test extension via the Orchestrator Extension Framework.
version: 0.1.0
maintainer: "Test Maintainer <maintainer@test.com>"
homepage: "https://github.com/maintainer/test"
source:
type: git
url: "https://github.com/maintainer/test"
refs: "branchName"
spec:
type: Orchestrator
compatibility:
cloudstack:
minVersion: 4.23.0
entrypoint:
path: test.py
targetDir: /usr/share/cloudstack-management/extensions/test
orchestrator:
requiresPrepareVm: false
details:
key1: value1
key2: value2
enabled: true
customActions:
- name: CreateSnapshot
displayName: "Create Snapshot"
description: "Create a snapshot for a virtual machine instance."
resourcetype: VirtualMachine
enabled: true
timeout: 600
allowedroletypes: [Admin, DomainAdmin, User]
parameters:
- name: snap_name
type: STRING
validationformat: NONE
required: true
- name: snap_description
type: STRING
validationformat: NONE
required: false
- name: snap_save_memory
type: BOOLEAN
validationformat: NONE
required: false
- name: ListSnapshots
displayName: "List Snapshots"
description: "List all snapshots for a virtual machine instance."
resourcetype: VirtualMachine
enabled: true
timeout: 120
allowedroletypes: [Admin, DomainAdmin, User]
parameters: []
- name: RestoreSnapshot
displayName: "Restore Snapshot"
description: "Restore a virtual machine instance from a given snapshot."
resourcetype: VirtualMachine
enabled: true
timeout: 900
allowedroletypes: [Admin, DomainAdmin, User]
parameters:
- name: snap_name
type: STRING
validationformat: NONE
required: true
- name: DeleteSnapshot
displayName: "Delete Snapshot"
description: "Delete a snapshot for a virtual machine instance."
resourcetype: VirtualMachine
enabled: true
timeout: 300
allowedroletypes: [Admin, DomainAdmin, User]
parameters:
- name: snap_name
type: STRING
validationformat: NONE
required: true

View File

@ -1274,6 +1274,7 @@
"label.images": "Images",
"label.imagestoreid": "Secondary Storage",
"label.import.backup.offering": "Import Backup Offering",
"label.import.extension": "Import Extension",
"label.import.instance": "Import Instance",
"label.import.offering": "Import Offering",
"label.import.role": "Import Role",
@ -1548,6 +1549,7 @@
"label.management.servers": "Management Servers",
"label.management.server.peers": "Peers",
"label.managementservers": "Number of management servers",
"label.manifesturl": "Manifest URL",
"label.matchall": "Match all",
"label.matchtype": "Match Type",
"label.max": "Max.",

View File

@ -86,6 +86,15 @@ export default {
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/extension/CreateExtension.vue')))
},
{
api: 'importExtension',
icon: 'cloud-upload-outlined',
label: 'label.import.extension',
docHelp: 'adminguide/extensions.html',
listView: true,
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/extension/ImportExtension.vue')))
},
{
api: 'updateExtension',
icon: 'edit-outlined',

View File

@ -0,0 +1,116 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-form
:ref="formRef"
:model="form"
:rules="rules"
:loading="loading"
layout="vertical"
@finish="handleSubmit">
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.manifesturl')" :tooltip="apiParams.manifesturl.description"/>
</template>
<a-input
v-model:value="form.manifesturl"
:placeholder="apiParams.manifesturl.description"
v-focus="true" />
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</div>
</template>
<script>
import { ref, reactive, toRaw } from 'vue'
import { postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
export default {
name: 'ImportExtension',
components: {
TooltipLabel
},
data () {
return {
loading: false
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('importExtension')
},
created () {
this.initForm()
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({})
this.rules = reactive({
manifesturl: [{ required: true, message: `${this.$t('message.error.input.value')}` }]
})
},
handleSubmit (e) {
e.preventDefault()
if (this.loading) return
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
this.loading = true
const params = {
manifesturl: values.manifesturl
}
postAPI('importExtension', params).then(response => {
this.$emit('refresh-data')
this.$notification.success({
message: this.$t('label.create.extension'),
description: this.$t('message.success.create.extension')
})
this.closeAction()
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message,
duration: 0
})
}).finally(() => {
this.loading = false
})
}).catch(error => {
this.$notifyError(error)
})
},
closeAction () {
this.$emit('close-action')
}
}
}
</script>
<style scoped lang="less">
.form-layout {
width: 80vw;
@media (min-width: 600px) {
width: 550px;
}
}
</style>